git cipher

🔐 Tools for storing encrypted content in Git

72
4
Ruby

git-cipher

Introduction

Gem Version

git-cipher is a utility script for encrypting sensitive files for storage in a public Git repo.

Usage

git cipher decrypt [FILES...] # decrypts files
git cipher encrypt [FILES...] # encrypts files
git cipher log [FILES...]     # shows log with (plaintext) diffs
git cipher ls                 # lists encrypted files
git cipher status             # shows decrypted/modified/missing status
git cipher help

Installation

git-cipher is a single Ruby script with no major dependencies beyond Ruby, Git, and GnuPG. As such, it can be run directly from the repo:

git clone https://github.com/wincent/git-cipher.git

Alternatively, you can install the Gem:

gem install git-cipher

Note: If you install the git-cipher executable somewhere in your $PATH, Git will treat it as a subcommand, which means you can invoke it as git cipher. Otherwise, you will have to provide the full path to the git-cipher executable.

To install the external prerequisites, use your preferred method. For example, on macOS you might choose to use Homebrew:

brew install git gnupg gpg-agent

Configuration

Selecting a key for encryption and decryption

git-cipher will encrypt files using a recipient key (public). Only the recipient can decrypt the files (using their private key). It is possible to select multiple, comma-separated recipients such that any one of them can decrypt the files, using their own key.

There are three ways to override the default recipient key (which is [email protected],[email protected] and therefore probably not useful to anybody other than me):

1. Set the GPG_USER environment variable

[email protected] git cipher decrypt

# or alternatively:
export [email protected]
git cipher decrypt

2. Use git-config to set cipher.gpguser

git config cipher.gpguser [email protected] # per-repo
git config --global cipher.gpguser [email protected] # globally

3. Edit DEFAULT_GPG_USERS in the git-cipher source

This last may be appropriate if you’ve installed by cloning the Git repo.

Usage on Arch Linux

For most of the lifetime of git-cipher, I’ve been using it on macOS, where everything works pretty much seamlessly out of the box, especially since moving to GnuPG v2 (see commit fd4c78aeb9d11 for more details of how things were streamlined by v2). On Arch Linux, I have found that I needed to do a bit of additional manual set-up.

The goal is to cache the secret key in the GPG agent so that you don’t have to re-enter the password for every file.

First of all, to find out the “keygrip” of the secret key:

  • gpg --with-keygrip -K (lists all secret keys)
  • gpg --fingerprint --with-keygrip [email protected] (lists a specific key; source)

Once you have the keygrip (eg. a string like 0551973D09...), you can allow it to be added as a “preset” passphrase (ie. cache it in the agent for the duration of the session). To achieve this, the following line:

allow-preset-passphrase

must be present in the ~/.gnupg/gpg-agent.conf file.

You then need to tell GnuPG how to prompt for a password using a “pin helper”. You can see which helpers are available with:

pacman -Ql pinentry | grep /usr/bin/

That will produce a list similar to:

pinentry /usr/bin/
pinentry /usr/bin/pinentry
pinentry /usr/bin/pinentry-curses
pinentry /usr/bin/pinentry-emacs
pinentry /usr/bin/pinentry-gnome3
pinentry /usr/bin/pinentry-gtk-2
pinentry /usr/bin/pinentry-qt
pinentry /usr/bin/pinentry-tty

Configure one to be used by adding to ~/.gnupg/gpg-agent.conf:

pinentry-program /usr/bin/pinentry-curses

After making any changes, reload the agent:

gpg-connect-agent reloadagent /bye

You can confirm which keys are known to the agent like so:

gpg-connect-agent 'keyinfo --list' /bye

Which will show a list like:

S KEYINFO 9BB6077848... D - - - P - - -
S KEYINFO A05D018ED6... D - - - P - - -
S KEYINFO 26CA5BE9E3... D - - - P - - -
S KEYINFO 4435E5FDCC... D - - - P - - -
S KEYINFO 0551973D09... D - - - P - - -
S KEYINFO 2529B67D84... D - - - P - - -

A 1 before the P shows that the key is cached in the agent, which means that none of the keys in the example list above are actually cached.

To actually cache the key, you can run:

/usr/lib/gnupg/gpg-preset-passphrase --preset 0551973D09...

If you redo the keyinfo --list operation, you should now see the expected 1:

S KEYINFO 9BB6077848... D - - - P - - -
S KEYINFO A05D018ED6... D - - - P - - -
S KEYINFO 26CA5BE9E3... D - - - P - - -
S KEYINFO 4435E5FDCC... D - - - P - - -
S KEYINFO 0551973D09... D - - 1 P - - -
S KEYINFO 2529B67D84... D - - - P - - -

Architecture

This section describes the background and rationale for the design of git-cipher.

git-cipher was originally developed within the context of an Ansible-powered configuration repo. Some of the files contained sensitive information so I couldn’t commit them to the repo as plaintext, but I did still want to version control them, so needed to encrypt them prior to committing.

At the time (prior to version 1.5) Ansible didn’t have the Vault feature (equivalent to Chef’s encrypted data bags), so the git-cipher was developed. When the Vault feature did arrive, I decided to continue using git-cipher because I liked the convenience of being able to work using my normal editor and Ansible workflow (Vault requires you to edit files through the ansible-vault command, and pass special arguments to ansible-playbook when running playbooks).

git-cipher stores encrypted versions of sensitive files in the Git repo with an .encrypted file extension. Before running Ansible commands, we decrypt the necessary files with git cipher decrypt. This works well because Ansible doesn’t employ a client-server architecture like Chef or Puppet; it suffices to have the decrypted files available on the local machine where the repo is checked out and the commands are run.

The plain-text versions of the encrypted files should be ignored via the gitignore mechanism (although that is only a recommendation and git-cipher does nothing to enforce the policy, beyond printing a warning when it sees that the plaintext version of an encrypted file is not being ignored).

This approach compares to using git encrypt as recommended in “An example of provisioning and deployment with Ansible”. There are a couple of problems with that approach; one is that it uses “deterministic encryption” in the name of convenience, while noting that it is insecure. Additionally, a lengthy thread on the Git mailing list argues that this is an abuse of the clean/smudge filtering system.

We use semantically secure GPG encryption and make use of timestamp comparisons to avoid unnecessary churn (in other words, we only update the encrypted version of a file if the plain text version is newer). Note that we effectively trust the local system’s integrity, relying on filesystem encryption, filesystem permissions, and the general security of the system to keep the plain-text safe. Also note that, like the clean/smudge filtering, we are effectively forgoing some of the niceties that Git offers (compression, meaningful diffs etc) in exchange for security. (But note: you can also split your encrypted content across smaller individual files and then pull the values from those files into a template, preserving the ability to get meaningful diffs of the template files themselves. The log subcommand also goes some way towards providing visualization of change over time in encrypted files.)

For an example of git-cipher usage in the wild, see files like this one in my dotfiles repo. Others in that repo can be found by searching for files with the .encrypted suffix.

Although git-cipher can be obtained as a RubyGem, the executable itself is, by-design, a single file. This is to reduce the dependency footprint, making it more suitable for use in boot-strapped environments (which might not have RubyGems installed yet).

Tips

You may see prompts like the following, depending on the trust level of your signing key:

It is NOT certain that the key belongs to the person named
in the user ID.  If you *really* know what you are doing,
you may answer the next question with yes.

You can avoid these prompts by setting the trust level to “ultimate” like this:

gpg --edit-key [email protected] # or $GPG_USER
> trust
> quit

Author

git-cipher was hacked together by Greg Hurrell ([email protected]).

Development

This is minimal, currently with no tests, no Bundler, no Rakefile. To cut a new release, update the version number in the gemspec and:

git tag -s 0.2 -m "0.2 release"
git push --follow-tags origin main
gem build git-cipher.gemspec
gem push git-cipher-0.2.gem

License

git-cipher is licensed under the MIT license. See the LICENSE.txt file in the git-cipher repo for more details.