Skip to main content

Sharing Passwords with Git, GPG and Pass

Many companies rely on tools such as LastPass or 1Password to manage and share passwords. These tools are very useful if you care about security, as of course you should but they also come with their own set of problems.

I have always been a bit frustrated using these and had been on the lookout for a better alternative. I wanted a tool that didn’t rely on a third-party and that could be used inside a terminal. pass filled this need perfectly and I have been a happy user for more than a year.

Contrary to other tools, pass (website) is a very simple command-line tool which means you can integrate it with pretty much anything. It encrypts passwords with your and potentially other people’s GPG keys and integrates with Git for sharing or simply synchronising between different computers.

Before you can start using pass, you need to set up GnuPG and generate keys if you don’t have any already. You can also use these keys to encrypt emails for example. This post is a good start if you want to dig deeper into GPG.


Setting up GnuPG

We will generate two keys (or rather pairs of keys): a master key and an encryption subkey. The (private) master key should ideally be kept offline for maximum security while the subkey can be stored on your computer. In case your system is compromised, the master key is still safe and it can be used to issue a revocation certificate for the subkey.

After having installed GnuPG using your favourite package manager, the command gpg2, or alternatively just gpg, should be available. We can now generate some keys. The most security-conscious people will want to do that offline, or even on a system that is never to be connected again.

$ gpg2 --full-generate-key
gpg (GnuPG) 2.2.17; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 5y
Key expires at Sat 23 Nov 2024 07:31:30 GMT
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Alice
Email address: alice@example.org
Comment:
You selected this USER-ID:
    "Alice <alice@example.org>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
#
# At this moment, a prompt pops up asking to choose a passphrase.
#
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: /home/alice/trustdb.gpg: trustdb created
gpg: key 2C8607E19D882192 marked as ultimately trusted
gpg: directory '/home/alice/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/alice/openpgp-revocs.d/2D73209777E12843C8483F4B2C8607E19D882192.rev'
public and secret key created and signed.

pub   rsa4096 2019-11-25 [SC] [expires: 2024-11-23]
      2D73209777E12843C8483F4B2C8607E19D882192
uid                      Alice <alice@example.org>
sub   rsa4096 2019-11-25 [E] [expires: 2024-11-23]

This command has generated a master key and an encryption key, both valid for 5 years. You might want to change that. I use 5 years for my master key and 1 year for subkeys. This post is however not about GnuPG, so I kept this section as short as possible.

Passwords not only need to be encrypted but also signed, so we need to add a new subkey for signature. The ID passed to the command is the one shown by gpg2 --list-keys. Note that you can set up Git to sign your commits with your key now if you wish to.

$ gpg2 --edit-key 2D73209777E12843C8483F4B2C8607E19D882192
gpg (GnuPG) 2.2.17; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  rsa4096/2C8607E19D882192
     created: 2019-11-25  expires: 2024-11-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa4096/81E8016ABB301F1E
     created: 2019-11-25  expires: 2024-11-23  usage: E
[ultimate] (1). Alice <alice@example.org>

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
Your selection? 4
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 1y
Key expires at Wed 25 Nov 2020 07:25:06 GMT
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  rsa4096/2C8607E19D882192
     created: 2019-11-25  expires: 2024-11-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa4096/81E8016ABB301F1E
     created: 2019-11-25  expires: 2024-11-23  usage: E
ssb  rsa4096/AC541E94C6016EE4
     created: 2019-11-26  expires: 2020-11-25  usage: S
[ultimate] (1). Alice <alice@example.org>

A quick tip before setting up pass: use shred -u instead of rm when deleting sensitive files from your disk such as private keys that you would transfer to a safer place. shred overwrites the file’s contents with random data and -u causes it to delete the file.


pass for personal use

First, the password store — the directory containing all encrypted passwords — needs to be initialised. It’s also best to synchronise it with Git to avoid losing access to all your accounts if you spill your coffee on your laptop.

$ gpg2 --list-keys
/home/alice/.gnupg/pubring.kbx
-------------------------------
pub   rsa4096 2019-11-25 [SC] [expires: 2024-11-23]
      2D73209777E12843C8483F4B2C8607E19D882192
uid           [ultimate] Alice <alice@example.org>
sub   rsa4096 2019-11-25 [E] [expires: 2024-11-23]
sub   rsa4096 2019-11-26 [S] [expires: 2020-11-25]

$ pass init 2D73209777E12843C8483F4B2C8607E19D882192
Password store initialized for 2D73209777E12843C8483F4B2C8607E19D882192

$ pass git init
Initialized empty Git repository in /home/alice/.password-store/.git/

$ pass git remote add origin git@github.com:alice/pass.git

It is now ready for new passwords to be generated. Let’s generate one for your bank account.

$ pass generate -c bigbank 30
[master (root-commit) 6c0f9b0] Add generated password for bigbank.
 4 files changed, 2 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gpg-id
 create mode 100644 bigbank.gpg
Copied bigbank to clipboard. Will clear in 45 seconds.

This will generate a 30-character random password, encrypt it with your public GPG key and — as you can see — create a new commit in the repository. pass git is simply a wrapper around git so you can use it as you normally do for other repositories.

The -c option is copying the password to your system clipboard for 45 seconds. Another useful option is --no-symbols to only alphanumerical characters.

Later on, you can retrieve the password by using pass show -c bigbank or just pass -c bigbank to copy it to the clipboard. Similarly, pass bigbang will print the password on stdout.

Let’s push the commit to Github.

$ pass git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 2.24 KiB | 2.24 MiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To git@github.com:alice/pass.git
 * [new branch]      master -> master

Storing additional information

pass -c bigbank will only copy the first line of the file to the clipboard. That means you can store the password on the first line and any other information in the rest of the file such as your username.

$ pass edit bigbank
[master ad24857] Edit password for bigbank using nvim.
 1 file changed, 0 insertions(+), 0 deletions(-)
 rewrite bigbank.gpg (100%)

This command opens your editor and allows you to edit the file. You can set the environment variable EDITOR to your editor of choice.


Replacing old passwords

Using a password manager doesn’t mean passwords don’t need to be changed on a regular basis. Issuing the same command as above will work but will overwrite the whole file, including any additional information that you may have added. The correct way to do it is to use --in-place.

$ pass generate --in-place -c bigbank 30
[master 097075e] Replace generated password for bigbank.
 1 file changed, 0 insertions(+), 0 deletions(-)
 rewrite bigbank.gpg (100%)
Copied bigbank to clipboard. Will clear in 45 seconds.

Sharing passwords with a team

Passwords of your organisation could be shared in their own repository which you could then configure as a Git submodule (or better, a Git subtree) of your personal password repository. That way, they would appear in ~/.password-strore/your-organisation/ and you could use pass -c your-organisation/admin. This approach still requires you (and other teammates) to do some maintenance though.

In this section, I’ll take a different approach where passwords are stored in a subdirectory of your project’s repository and I will use direnv (website) to automatically set things up.

First, we need to create the password store and initialise it with the public keys of team members.

$ PASSWORD_STORE_DIR=~/awesome-project/secrets pass init 2D73209777E12843C8483F4B2C8607E19D882192
mkdir: created directory '/home/alice/awesome-project/secrets/'
Password store initialized for 2D73209777E12843C8483F4B2C8607E19D882192

As you can see, the environment variable PASSWORD_STORE_DIR allows us to specify a different directory for the password store. Having to remember to set this variable every time you want a password would be annoying though, so let’s create a .envrc file at the root of the repository.

#!/usr/bin/env bash

set -euxo pipefail

export PASSWORD_STORE_DIR="$PWD/secrets"

After running direnv allow, this script will automatically be picked up and ran by direnv when you cd into the repository.

Let’s commit these.

$ git add -p .envrc secrets/.gpg-id

$ git commit -m "Initialise password store"
[master (root-commit) b13567d] Initialise password store
 2 files changed, 6 insertions(+)
 create mode 100644 .envrc
 create mode 100644 secrets/.gpg-id

Adding and removing team mates

After having set up GnuPG, Bob, a new team member, can communicate his public key to the rest of the team. The public key can be exported with gpg.

$ gpg2 --export --armor 4C7D8DE51D2780E898B0BF230B48F5750A41A46B > ~/publickey

Alice can then import it, tell GPG she trusts the key really belongs to Bob and add the key to the password store.

$ gpg2 --import ~/publickey
gpg: key 0B48F5750A41A46B: public key "Bob <bob@example.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1

$ gpg2 --edit-key 4C7D8DE51D2780E898B0BF230B48F5750A41A46B
# ...
> trust
# ...
# pass requires an ultimate trust in the keys used to encrypt passwords
Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)

  1 = I don't know or won't say
  2 = I do NOT trust
  3 = I trust marginally
  4 = I trust fully
  5 = I trust ultimately
  m = back to the main menu

Your decision? 5
Do you really want to set this key to ultimate trust? (y/N) y
# ...

$ pass init `cat secrets/.gpg-id` 4C7D8DE51D2780E898B0BF230B48F5750A41A46B
Password store initialized for 2D73209777E12843C8483F4B2C8607E19D882192, 4C7D8DE51D2780E898B0BF230B48F5750A41A46A
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2023-07-23
example: reencrypting to 81E8016ABB301F1E DD2FFD3598EBCD77

pass re-encrypts passwords with the new key. And that’s it. You only one push away from sharing secrets with somebody new.

But now imagine Bob was only hired for a mission of 6 months. What happens then?

$ pass init `cat secrets/.gpg-id | grep -v 4C7D8DE51D2780E898B0BF230B48F5750A41A46B`
Password store initialized for 2D73209777E12843C8483F4B2C8607E19D882192
example: reencrypting to 81E8016ABB301F1E

Removing somebody from the team is a bit more complicated than that, though. Indeed, simply removing the key is not enough. That person might have a copy of the repository on her laptop with passwords encrypted with his public key or have copied them somewhere else. There is only one solution: change your passwords.

One last thing worth noting. Scripts in the repository can now make use of encrypted passwords by calling pass as follows.

#!/usr/bin/env bash

set -euxo pipefail

some-command --user developer --password "$(pass example)"

Ecosystem around pass

On top of the very simple command-line tool, there is a whole bunch of extensions and programs built on top of it as well of migrators.

The only one I personally use is passmenu, a dmenu script. I use dmenu together with XMonad. After pressing Super+p, I can type passmenu and access all my passwords. This is so convenient that it replaces any need for browser extensions and such.

There is an extension for OTP (One-Time Password, read 2-factor authentication), although it completely defeats the point of 2FA if you have both the password and the OTP in the same password manager.

Speaking of which, there are extensions for Firefox and Chrome. If you are on Mac OS X, you can add it to Alfred. There is also a client for iOS and Android.

If (like me) you are not comfortable with sharing all your passwords with your phone, you can easily share a single password when you really need to by using pass -q example. It will show a QR code with the password so you don’t have to type it manually.

You can find the full list on the website.


Shortcomings of pass

Of course, pass is still a niche tool. Not everybody is ready or capable to handle GPG keys and Git. There is however no reason you couldn’t use both pass for developers and a more traditional password manager for the rest of the business.

Even for developers and other technically-inclined people, it comes for a steeper — although far from insurmountable — learning curve.

The main shortcoming is the difficulty of handling GPG keys. It is a fair amount of work which you might not want or have the time to do. It is not however entirely lost as they can be used to encrypt files and emails, sign Git commits and such.


As you have probably have understood by now, it doesn’t come for free but can very well be worth it.