pass-gocrypt: Yet Another Metadata Encryption Extension for Pass
November 8, 2022•2,715 words
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.
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:
- pass-tomb, which stores the entire
.password-storedirectory inside a Tomb, a wrapper script built upon LUKS. This, however, requires root permissions, since the use of device mapper is limited to the superuser. The use of DM also limits its potential usage to Linux only, with little to no hope for cross-platform compatibility (unless someone digs into the undocumented internals of DM and re-implements everything bug-for-bug on another platform).
- pass-grave, storing the entire password store inside a
.tar.gzfile encrypted with GPG. This does not require root privileges like
taris a portable format, unlike LUKS. However, this does mean that every time we need to decrypt the password store, all files must be extracted from the tar and physically written to the underlying filesystem, and vice-versa when closing the password store. Not only is this not very elegant, it also creates a potential leakage if the underlying filesystem does not truly erase deleted data (which is the case for most filesystems).
- pass-coffin, another extension very similar to
pass-grave. It also puts the entire password store inside a tarball wrapped within a layer of GPG encryption. As far as I can tell,
pass-coffinis essentially the same as
pass-grave, with the only difference being the exposed command line interface. It also aims for better code quality than other extensions, but this is beyond the scope here.
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:
- They break
pass gitfunctionalities. Although all three of the solutions mentioned above do still technically allow the usage of
pass git, it only works with the decrypted form of the password store. Even though some of the authors (such as the author of
pass-coffin) urge users to back up their password stores using
git, the repository that is pushed to a remote is decrypted, in its original form, defeating what I see as one of the main goals of adding another layer of metadata encryption. I am personally more worried about a remote git repository leaking metadata of my password store than an unencrypted directory on my disk would do.
- They still fully rely on the security of GPG only. As mentioned earlier, this may or may not be an issue for you, but I would not mind if there was an option to add a standalone symmetric passphrase to the encrypted password store, such that accessing it requires both the gpg private key and the symmetric passphrase.
- They operate under a "all-or-nothing" model. Because none of the solutions work on mobile platforms like Android as of now (also partly due to the reliance on LUKS in the
pass-tombcase), it means that metadata encryption and mobile support is mutually exclusive. Ideally, the user should be able to choose which entries to encrypt under the metadata encryption scheme, while leaving the rest as-is -- as mentioned before, you probably do not need all of the entries to be hidden behind metadata encryption. These entries without metadata encryption would be accessible on all platforms and devices. This, combined with the extra symmetric passphrase, also allows one to set up an effective "trust hierarchy" within one single password store, where devices holding the repository would not necessarily be able to read the metadata of the full password store -- at least without the extra passphrase.
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:
- Provides a reasonable level of metadata encryption within a single password store, such that it would play nicely with scripts and browser plugins based on
- Must play nicely with
pass git, i.e. content after encryption must be possible to synchronize using Git just like without metadata encryption
- Must be able to encrypt only a user-selected subset of entries
- Must not generate an excess of filesystem I/O during normal operation
- Must support adding a symmetric passphrase for extra protection
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.
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-grave do to the password store.
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
Integration with Pass
I then set out to write an extension for
pass, pass-gocrypt, that integrates
pass with a convenient command-line interface.
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.
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-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
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.
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.
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
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