Milhouse on software, engineering, and Emacs.

Writing your own password manager with gnupg and shell

TL;DR: In this post I will walk through the implementation of a simple, yet complete password manager in pure shell script. This may seem a daunting task, but as you will see, the important bits are already solved by gnupg, and all we need to do is to write some “glue” code.

Password managers

Passwords are important. I think I don’t need to convince you of that. Chances are that you never really though much about your password’s safety, and if you never did so, I recommend you to read this great post by Jeff Atwood (of coding horror) to understand why you should care.

There are many services that provide you “password management in their cloud”, like Zoho’s Vault, Lastpass, 1Password. (EDIT: Actually, 1Password does not store your passwords as correctly noted by @fnando in the comments. Sorry for the bad research :/). I never really used any of these, and the very idea of storing my passwords behind a vendor’s wall cloud makes me cringe. (for no good reason actually. I’m not a security person). Also, none of these services are free (as in beer and as in speech), and they are too much expensive for my taste.

As Jeff Atwood explains, your password should be long and random in order to give a bad time for anyone trying to crack it. We will be able to generate, store and remember passwords with, say, 64 random characters with the simple script we are going to write.

What are we going to do then?

Password management should be simple. In our solution, each password will live inside a gpg encrypted file whose name is the name of the password (Oh, RLY?). Being plain-text files, these files can be moved from computer to computer without any hassle. I do keep mine on Copy.

The minimum set of features we expect from our password manager are:

In order to secure the storage of our passwords, we are going to use gnupg and gpg-agent which are part of the GNU project and are Free (as in beer and as in speech).

First of all, what is this GPG thing?

If you’re not familiar with what PGP and GnuPG are, check the source of all knowledge.

Pretty Good Privacy (PGP) is a data encryption and decryption computer program that provides cryptographic privacy and authentication for data communication. PGP is often used for signing, encrypting, and decrypting texts, e-mails, files, directories, and whole disk partitions and to increase the security of e-mail communications. It was created by Phil Zimmermann in 1991.

PGP and similar software follow the OpenPGP standard (RFC 4880) for encrypting and decrypting data.

The Free Software Foundation has developed its own OpenPGP-compliant program called GNU Privacy Guard (abbreviated GnuPG or GPG). GnuPG is freely available together with all source code under the GNU General Public License (GPL) …

Wikipedia wizards

There is also this great introduction on how to use gnupg to encrypt your important stuff.

Encrypting a password (password-set)

For now on, I will assume you have a working gpg-agent setup. That means the passphrase for the recipient’s (which is yourself) key is cached, your private and public keys are trusted and are in the gpg-agent’s keyring.

First, we need to define where the passwords are going to be stored and who is the “recipient” of the gpg encrypted text. We will store this information in environment variables:

export MIMIPASS_HOME=~/Copy/.secrets/
export MIMIPASSS_RECIPIENT=renanranelli@gmail.com

The options needed to encrypt some text with gpg are:

gpg --recipient $RECIPIENT --armor --output $OUTFILE --encrypt $INFILE

The --armor when given will generate the encrypted file only with printable characters.

All we still lack is to provide is the name of the password we want to set:

mimipass-set() {
    passwd_file=$MIMIPASS_HOME/$1.gpg
    read -ep 'password: '
    gpg -r $MIMIPASS_RECIPIENT -a -o $passwd_file -e <(echo $REPLY)
}

Here our INTPUTFILE is a named pipe generated by the <(...) bash syntax (this thing is called process substitution), which allow you to treat an arbitrary command output as a regular file (a named pipe actually).

$ mimipass-set test       # ops. We forgot to set it. let's do it now
# => Write down your password... C-d when done
# => 1234
# => Done!

You can check now that a file $MIMIPASS_HOME/test.gpg has been created with contents resembling this:

-----BEGIN PGP MESSAGE-----
Version: GnuPG v1

hQEMA/I6COc700iRAQf+MIpyc1o8W/zZMT+Y+U0QbIL0VDMMGj9gUVROweRpH0ei
26ydtWeVRX+rT4wVXE5ZcLWxOMOmKx6FlJYeTIZnetOxg5+8pJy7jfXhlH0gxt3v
/Vi70qzeTnyDLb9YIKrBzd60L0h16+atSO1pD1va+myTCz2B8oVjDYB0aHP5hEOk
c/mpBARyEl1HT+NqP+duRe5GzfUkVBy4B6MXqAQa5GCyrcFFKAbsQKlW2QEalVcH
wiWziIwApkiaAv56zjx2Lm6cN5FwfqvkpqeiPdPIWLea5rMPBKjgYOzUs70z9BAZ
IFlua/EE5lw6Oz9BYZunCoVX2MS4lJQHVWC6Hz2h89JBAQVn/2hOA+mQ4QKYuXDj
fmFQMs26l2y/esTgMdn3gC3WQi3LQCwTxlL0vHiXkmLZCYeaQ27qpdTToOlvxbfj
T3E=
=BYID
-----END PGP MESSAGE-----

With the encryption side taken care of, we then focus on recovering the previously stored password.

Recovering the password

To decrypt the contents of the file we just created, the gpg command is:

gpg --quiet --no-tty --use-agent --recipient $RECIPIENT --decrypt $FILE

The names of the parameters are quite descriptive so I won’t comment anything about them. Again, the only thing we need to provide is the name of the password we want to recover:

mimipass-get() {
    passwd_file=$MIMIPASS_HOME/$1.enc

    if [ -f $passwd_file ]; then
        gpg -q --no-tty --use-agent -r $MIMIPASS_RECIPIENT -d $passwd_file
    else
        err "Couldn't find [ ${passwd_name} ]"
    fi
}

Now, in order to recover our test password:

$ mimipass-get test
# => 1234

Easy peezy right? But having to select & copy the password from the terminal is quite tedious. We can send the password directly to the clipboard using xclip

mimipass-copy() {
    mimipass-get $1 | xclip -i -selection clipboard \
        && echo "Password for $1 sent to the clipboard."
}

And calling it:

$ mimipass-copy test
# => Password for $1 sent to the clipboard.

You’re now have your password in your C-v (or C-y if you use the best text editor :troll:).

After creating a bunch of passwords, we need to check out which passwords we have in store. Let’s write now the password listing feature.

Listing existing passwords

This one is easy, and we only need shell globbing:

mimipass-list() {
    ls -1 $MIMIPASS_HOME/*.gpg \
        | xargs -I{} -n1 basename {} .gpg \
        | xargs -n1 echo "- {}"
}

Calling it we see:

$ mimipass-list
# => - test
$ echo 1234 | mimipass-set test2
$ mimipass-list
# => - test
# => - test2

Awesome. Let’s move on to the last feature: generating a new password.

Generating a random password

There are many available solutions to generating random passwords. I’m no expert, so I won’t talk about them. I am going to use openssl to generate the random password.

You can generate a random string of 128 printable characters with openssl using the following command:

$ openssl rand -base64 128
# => 7wc1cq9xygJS4OKdTg4ALVEOLOqNP9E1mFT3M8mxH2+snn5hKYzf8eIsLLDHYEj9
# => e805ZJKcOCVPY3MjyEaxqit9aTN71NhNHKbTDtfF5mcMUv4O+3NzwECbyVZqpfNi
# => e0HnAMkdxbd/EndoKpDzYnN1s26zXpFzGCZIo7FpdMM=
# =>

You probably don’t want 128 characters, and the newlines are annoying in the middle of a password. It would also be nice to define the size of the string beforehand. Easy:

$ size=12
$ openssl rand -base64 128 | tr -d '\n' | cut -c -$size
# => BdmpTxqpuJ

All we need to do now to generate a new password is to pipe the output of the previous command to our mimipass-set function:

mimipass-new() {
    passwd=$1
    size=${2:-64}

    openssl rand -base64 128 \
        | tr -d '\n' \
        | cut -c -$size \
        | mimipass-set $passwd
}

Checking that it works:

$ mimipass new-set test2  # generate a new password and set it to `test2`
$ mimipass get test2      # it works!
# => OVv5FQi5maQlgrAfJtn8E+rldsGNgfazrbF/HLX4WvskwHpmm8wiPuxIRq96Edy+

And that’s it. We have covered all the features we listed in less than 40 lines of shell script. We were able to do this because gpg-agent did all the heavy lifting for us.

The code presented here is available at github with some minor modifications

EDIT: After I implemented MimiPass I found pass, which embodies the same idea (i.e., is gpg-agent based) but is much more mature and has some extra features. I definitely recommend you to check it out, since there are many niceties and integrations available.

That’s it.

Comments