Predictable, passphrase-based PGP key generator
passphrase2pgp generates, in OpenPGP format, an EdDSA signing key and Curve25519 encryption subkey entirely from a passphrase, essentially allowing you to store a backup of your PGP keys in your brain. At any time you can re-run the tool and re-enter the passphrase to reproduce the original keys.
The keys are derived from the passphrase and User ID (as salt) using Argon2id (memory=1GB and time=8) and RFC 8032. It's aggressive enough to protect from offline brute force attacks passphrases short enough to be memorable by humans. Always use a strong passphrase.
Requires Go 1.9 or later.
$ go get -u github.com/skeeto/passphrase2pgp
Quick start: Provide a user ID (
-u) and pipe the output into GnuPG.
$ passphrase2pgp -u "Real Name <firstname.lastname@example.org>" | gpg --import
-l) is required.
-u) option supplies the user ID string for the key to be generated. If
--uidis missing, the
-l) option loads a previously generated key for use in other operations (signature creation, ASCII-armored public key, etc.).
There are two commands:
Key generation (
-K) [default]: Writes a key to standard output. This is a secret key by default, but
-p) restricts it to a public key.
Detached signatures (
-S): Signs one or more input files. Unless
--loadis used, also generates a key, but that key is not output. If no files are given, signs standard input to standard output. Otherwise for each argument
file.sigwith a detached signature. If armor is enabled (
-a), the file is named
-h) for a full option listing:
Usage: passphrase2pgp -K <-u id|-l key> [-anpsvx] [-i ppfile] [-r n] [-t time] passphrase2pgp -S <-u id|-l key> [-av] [-i ppfile] [-r n] [files...] Commands: -K, --key output a key (default) -S, --sign output detached signatures Options: -a, --armor encode output in ASCII armor -c, --check KEYID require last Key ID bytes to match -h, --help print this help message -i, --input FILE read passphrase from file -l, --load FILE load key from file instead of generating -n, --now use current time as creation date -p, --public only output the public key -r, --repeat N number of repeated passphrase prompts -s, --subkey also output an encryption subkey -t, --time SECONDS key creation date (unix epoch seconds) -u, --uid USERID user ID for the key -v, --verbose print additional information -x, --paranoid increase key generation costs
Per the OpenPGP specification, the Key ID is a hash over both the key
and its creation date. Therefore using a different date with the same
passphrase/ID will result in a different Key ID, despite the underlying
key being the same. For this reason, passphrase2pgp uses Unix epoch 0
(January 1, 1970) as the default creation date. You can override this
-n), but, to regenerate the same key
in the future, you will need to use
--time to reenter the exact time.
If 1970 is a problem, then choose another memorable date.
-c) causes passphrase2pgp to abort if the final bytes
of the Key ID do not match the hexadecimal argument. If this option is
not provided, the
KEYID environment variable is used if available. In
-r) is set to zero unless it was explicitly
provided. The additional passphrase check is unnecessary if they Key ID
is being checked.
-x) setting quadruples the KDF difficulty. This will
result in a different key for the same passphrase.
Generate a private key and send it to GnuPG:
$ passphrase2pgp --uid "..." | gnupg --import
Create an armored public key for publishing and sharing:
$ passphrase2pgp --uid "..." --armor --public > Real-Name.asc
--uid from the environment so that you don't need to type it
out every time you use passphrase2pgp:
$ export REALNAME="Real Name" $ export EMAIL="email@example.com" $ passphrase2pgp -ap > Real-Name.asc
Generate a private key and save it to a file in OpenPGP format for later use below:
$ passphrase2pgp --uid "..." > secret.pgp
Created detached signatures (
-S) for some files:
$ passphrase2pgp -S --load secret.pgp document.txt avatar.jpg
This will create
avatar.jpg.sig. The other end
would use GnuPG to verify the signatures like so:
$ gpg --import Real-Name.asc $ gpg --verify document.txt.sig $ gpg --verify avatar.jpg.sig
Or, in order to avoid entering the passphrase again and waiting on key generation, use the previously saved private key to sign some files without entering your passphrase:
$ passphrase2pgp -S --load secret.pgp document.txt avatar.jpg
Same, but now with ASCII-armored signatures:
$ passphrase2pgp -S --load secret.pgp --armor document.txt avatar.jpg $ gpg --verify document.txt.asc $ gpg --verify avatar.jpg.asc
Interacting with GnuPG
Once your key is generated, you may want to secure it with a protection passphrase on your GnuPG keyring in order to protect it at rest:
$ gpg --edit-key "Real Name" gpg> passwd
Trust is stored external to keys, so imported keys are always initially untrusted. You will likely want to mark your newly-imported primary key as trusted.
$ gpg --edit-key "Real Name" gpg> trust
Or use the
--trusted-key option in
Signing Git tags and commits
It's even possible to use passphrase2pgp directly to sign your Git tags
and commits. Just configure
gpg.program to passphrase2pgp:
$ git config --global gpg.program passphrase2pgp
However, with this setting you will be unable to verify commits and
tags. To work around this problem, wrap passphrase2pgp in a script like
the following, with options adjusted to taste (add
#!/bin/sh -e if [ "$2" != -bsau ]; then exec gpg "$@" # fallback GnuPG when not signing fi passphrase2pgp --sign --armor --uid "$3" printf '\n[GNUPG:] SIG_CREATED ' >&2
gpg.program to this script instead:
$ git config --global gpg.program path/to/script
This does just enough to convince Git that passphrase2pgp is actually GnuPG. Example session of signing a tag with passphrase2pgp:
$ git tag -s tagname -m 'Tag message' passphrase: passphrase (repeat):
Tag verification (via script to fallback to GnuPG):
$ passphrase2pgp -u "..." -p | gpg --import passphrase: passphrase (repeat): $ git verify-tag tagname gpg: Good signature from ...
Isn't generating a key from a passphrase foolish? If you can reproduce your key from a passphrase, so can any one else!
In 2019, the fastest available implementation of Argon2id running on the best available cloud hardware takes just over 6 seconds with passphrase2pgp's default parameters. That's 6 seconds of a dedicated single CPU core and 1GB of RAM for a single guess. This means that at the current cloud computing rates it costs around US$50 to make 2^20 (~1 million) passphrase guesses.
A randomly-generated password of length 8 composed of the 95 printable ASCII characters has ~52.6 bits of entropy. Therefore it would cost around US$ 158 billion to for just a 50% chance of cracking that passphrase. If your passphrase is generated by a random process, and it's at least this long, it is not the weak point in this system.
Since OpenPGP encryption is neither good nor useful anymore, I considered not generating an encryption subkey. The "privacy" portion of OpenPGP has become the least important part.
OpenPGP digital signatures still have some limited use, mostly due to momentum and lack of alternatives. The OpenPGP specification is over-engineered, loaded with useless legacy cruft, and ambiguous in many places. GnuPG is honestly not that great of an OpenPGP implementation, and I have low confidence in it. I stubbed my toe on a number of minor GnuPG bugs when hammering it with (usually invalid) output from passphrase2pgp while it was being developed.
- RFC 4880: OpenPGP Message Format
- RFC 6637: Elliptic Curve Cryptography (ECC) in OpenPGP (incomplete / inaccurate)
- RFC 7748: Elliptic Curves for Security
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC Draft: EdDSA for OpenPGP
- RFC Draft: OpenPGP Message Format (incomplete / inaccurate)