pass-gocrypt: Yet Another Metadata Encryption Extension for Pass

I have switched to pass, the password manager that conforms to the Unix philosophy, since a few months ago. It stores passwords as a bunch of .gpg files laid out in a file system directory, optionally being a git repository, and provides a bunch of shorthand commands for password generation and management. Compared to other fancy password managers with intricate UI / UX, pass feels very refreshing due to its simplicity and portability. You can use it basically anywhere where you have coreutil commands and gpg — and even on platforms without standard UNIX tools, it is pretty straightforward to create a platform-specific UI for it, such as the Password Store app on Android.

There is one big issue with pass, however, and that is metadata leakage. Because of its use of simple Unix directories and files, even though your passwords themselves are encrypted, metadata such as sites and apps you use is leaked in plaintext as the file and directory names. Depending on the specific directory structure you adopt, it could also leak the account IDs that you use on each service. This may or may not be a problem — for most services, your online presence there is probably already public knowledge. This applies to email, social networks, Git hosting, forums, and pretty much everything with user-generated content. But there is a substantial subset of services, at least for me, where my presence is not and shall not be public knowledge. For example, the exact hostnames of my personal servers, the services I host on them and my credentials for those services should preferably be kept private.

Another issue with pass is the use of asymmetric cryptography only — it is convenient when the asymmetric key can be stored on a hardware token like YubiKey, and to be frank, it may be more secure than a symmetric passphrase (when the private key is stored on a hardware token only). However, no asymmetric cryptography in use today is quantum-resistant, and depending on your exact threat model, this could be an issue. It is certainly not an issue for me at this point, but I would not mind if the password store can be protected by another layer of symmetric cryptography, just in case generic quantum computing becomes a reality way faster than anybody thinks it would — although it does not seem to be likely anytime soon.

Existing Solutions

Now, of course, since pass has existed for more than just a few days, and the fact that pass, being just a shell script, is very extensible, there have been a lot of solutions to these issues:

As you may see, all of these have at least one deal-breaker for me, be it the dependence on root privileges and LUKS, or the fact that every open / close operation generates a huge bunch of actual filesystem I/O that may even leak the metadata we are trying to protect in the first place. That is not even considering at least three common issues of these solutions:

Design Goals

I love the simplicity of pass, but I also love to not have to worry about metadata leakage. Since none of the existing solutions really meet my needs, I set out to design and implement one myself, just like how most OSS projects are created. Specifically, I want something that:

But, as I am far from a cryptographer or security engineer, I would not trust myself to roll a encryption scheme like this. That would be essentially equivalent to (or even harder than) inventing a full password manager all over again. Instead, just like what the author of pass-tomb did, I started searching around for something that I can build upon to implement these features.

Gocryptfs

Enter gocryptfs. Unlike LUKS, used by Tomb, gocryptfs is a userspace implementation of file-based encryption. It operates on a directory tree with a bunch of sub-directories and files, instead of on fixed-size block devices like LUKS does. Being implemented on top of FUSE, it also eliminates the need for root privileges, at least when unprivileged FUSE is allowed.

The nice thing about gocryptfs is that you can use it to set up an encrypted subtree anywhere on the filesystem, and of course this includes inside a pass password store. With the passphrase, an unprivileged FUSE mount would immediately provide a decrypted view into the encrypted subtree, without any actual I/O (other than gocryptfs metadata reads) being done. This is immediately way better than Tomb, and a LOT better than the very, very excessive I/O that pass-coffin and pass-grave do to the password store.

Because gocryptfs operates on files, not blocks on a block device, it would play very nicely with git by simply setting up the encrypted subtree somewhere inside the original pass repository. Of course, git will not be able to track changes inside encrypted files as diffs, but it could not do so to begin with inside a pass tree, so this is not really a disadvantage. gocryptfs itself works on Linux and macOS using libfuse, while a port to Windows also exists as cppcryptfs thanks to the simple and documented encryption scheme designed by the authors of gocryptfs. Even an Android port for gocryptfs exists as DroidFS, although it is not clear at this point how one could integrate this with the Password Store app.

gocryptfs relies on proven symmetric cryptography only. For our purposes, we could easily store the symmetric key inside a GPG-encrypted file for seamless operation with the rest of pass, or we can also add another passphrase to use along with the GPG encrypted one for extra protection. To put it simply, gocryptfs seems like the perfect choice to implement metadata encryption for pass.

Integration with Pass

I then set out to write an extension for pass, pass-gocrypt, that integrates gocryptfs with pass with a convenient command-line interface.

As both pass itself and its extensions are just shell scripts, pass-gocrypt is also pretty simple to implement. What I needed to do is basically a command to initialize the subtree encrypted by gocryptfs, a command to mount (open) it and another command to close it and return to an encrypted state. A few more shorthand commands would be nice, but these three are more than enough.

The gocryptfs subtree will always be located inside the .gocrypt subdirectory in the password store, and the decrypted version will be mounted at gocrypt (without the leading dot), also directly inside the directory of the password store. To initialize the .gocrypt directory, the extension simply invokes the existing pass generate command (albeit internally) to produce a random password, with a length of 32, named gocrypt-passwd inside the store. This password, optionally concatenated with another user-provided static passphrase, will be passed along to gocryptfs as the symmetric password for encryption.

Originally, I was to use some sort of key derivation to combine the user-provided passphrase with the randomly generated password protected by GPG. However, it was a pain to use any sort of proper KDF in bash, and I was reluctant to depend on Perl or Python for that. In addition, I realized that gocryptfs already uses scrypt (a type of KDF) internally before using the passphrase as any sort of encryption key, so a simple concatenation outside of gocryptfs should already be enough. As there is no real limit on the input length of scrypt, the concatenated password should already have all the properties we need.

After creating the gocrypt subtree with pass gocrypt init, you can then simply mount the encrypted subtree with pass gocrypt open, and after a while, unmount it with pass gocrypt close. This style of command line interface closely mimics those provided by pass-tomb and pass-coffin, and should be pretty straightforward to most users. When an extra symmetric passphrase is needed, the user will be prompted when running pass gocrypt open.

When the encrypted subtree is opened, all entries will appear under the gocrypt/ subdirectory inside the password store. This makes it compatible to all tools expecting the standard pass store format, albeit with a gocrypt/ prefix. All pass commands will work transparently inside the gocrypt/ prefix. However, for convenience, and for git compatibility (as will be mentioned later), you can (and should) operate inside the subtree using pass gocrypt <pass-command>. For example, instead of pass edit gocrypt/My/Password, you can use pass gocrypt edit My/Password. Notice how the second command does not include the gocrypt/ prefix, but has to be invoked under the gocrypt subcommand. All known pass commands have an equivalent one under the gocrypt subcommand, which will operate inside the subtree encrypted by gocrypt only.

git compatibility is a little bit tricky still, unfortunately. Don't get me wrong, git itself works perfectly fine with the encrypted subtree. The problem is that when you use the pass commands to generate or edit passwords, a corresponding git commit will be generated with the name of the file in its description (and, the generated commit may not have the correct files staged inside the encrypted directory). This defeats the purpose of implementing metadata encryption. As there is no way for pass extensions to hook into the generation of commits, the only way to work around this is to require the use of the wrapper commands (as mentioned earlier, the pass gocrypt <pass-command> family). These commands suppress the generation of the default commits, and instead creates git commits on their own with a generic commit message.

As an added bonus, these wrapper commands also enable nested repositories for us — the encrypted subtree itself can be its own standalone git repository! By simply running pass gocrypt git init, you can create an entirely separate git repository just for the encrypted subtree. This repository would not need to be pushed / synchronized on its own, because all of its state (.git) will be automatically included in the outer repository in the encrypted form. Doing so allows the history of the subtree to be tracked separately inside the encrypted directory, without leaking it to the history of the outer repository. After a nested repository is created, all pass gocrypt <pass-command> commands will generate two git commits: one inner commit that includes the detailed file names and changes, stored in the encrypted subdirectory; and another one generic outer commit that includes all the changes inside the encrypted subtree, along with its corresponding (encrypted) .git history changes.

By this point, all of the features I needed (as laid out earlier) have been implemented. The rest is just a few quality-of-life improvements, such as the ability to close itself after a given timeout using systemd-run, and a shorthand to move passwords between the encrypted / unencrypted subtrees. Please refer to the git repository for the latest list of features.

Using pass-gocrypt

To install pass-gocrypt, you simply need to copy gocrypt.bash to the correct path. This can be the .extensions subdirectory in your password store, or one of the supported system-wide extension paths. Please refer to the documentation of pass for details.

Here is a sequence of commands that demonstrate the usage of pass-gocrypt. What is laid out below is basically an excerpt from the project README.

To initialize:

pass gocrypt init
# With extra passphrase:
# pass gocrypt init -p

To generate a password inside the encrypted subdirectory:

pass gocrypt generate "My/Password"

To view a password from inside the encrypted subdirectory:

pass gocrypt show "My/Password"
# or simply: pass show "gocrypt/My/Password"
# or from any other pass-compatible GUI, when the subdirectory is opened

To move a password from outside of the encrypted subdirectory to inside:

pass gocrypt crypt "My/Insecure Password"

To close (unmount) the encrypted subdirectory:

pass gocrypt close

To re-open (mount) the encrypted subdirectory:

pass gocrypt open

To make the encrypted subtree a git repository of its own:

pass gocrypt git init

Conclusion

pass-gocrypt is a simple yet powerful tool to improve the security of pass, the standard password store for Unix. Although it is currently only tested on Linux, it should work with minimal changes on macOS and Windows (patches welcome). As gocryptfs is available on Android, it should be possible to extend Android Password Store with pass-gocrypt support, too, although this has not yet been done. Nevertheless, since we only rely on proven and documented cryptography, I expect it to be pretty straightforward to port and extend on any platform — just like the original pass tool.