• Stars
    star
    168
  • Rank 225,507 (Top 5 %)
  • Language
    Go
  • License
    The Unlicense
  • Created about 11 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Generate a PGP key from a passphrase

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.

See also: Long Key ID Collider

Installation

$ go install nullprogram.com/x/passphrase2pgp@latest

Remember to add $(go env GOPATH)/bin (or $GOBIN) to your $PATH.

Usage

Quick start: Provide a user ID (-u) and pipe the output into GnuPG.

$ passphrase2pgp -u "Real Name <[email protected]>" | gpg --import

Either --uid (-u) or --load (-l) is required.

  • The --uid (-u) option supplies the user ID string for the key to be generated. If --uid is missing, the REALNAME and EMAIL environmental variables are used to construct a user ID, but only if both are present.

  • The --load (-l) option loads a previously generated key for use in other operations (signature creation, ASCII-armored public key, etc.).

There are three commands:

  • Key generation (--key, -K) [default]: Writes a key to standard output. This is a secret key by default, but --public (-p) restricts it to a public key.

  • Detached signatures (--sign, -S): Signs one or more input files. Unless --load is 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 creates file.sig with a detached signature. If armor is enabled (--armor, -a), the file is named file.asc.

  • Cleartext signature (--clearsign, -T): Cleartext signs standard input to standard output, or from a file to standard output. The usual cleartext signature caveats apply.

Use --help (-h) for a full option listing:

Usage:
   passphrase2pgp <-u id|-l key> [-hv] [-c id] [-i pwfile] [--pinentry[=cmd]]
       -K [-anps] [-e[n]] [-f pgp|ssh|x509|signify] [-r n] [-t secs] [-x[spec]]
       -S [-a] [-r n] [files...]
       -T [-r n] >doc-signed.txt <doc.txt
Commands:
   -K, --key                 output a key (default)
   -S, --sign                output detached signatures
   -T, --clearsign           output a cleartext signature
Options:
   -a, --armor               encode output in ASCII armor
   -c, --check KEYID         require last Key ID bytes to match
   -e, --protect[=ASKS]      protect private key with S2K
   -f, --format FORMAT       select key format [pgp]
   -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
   --pinentry[=CMD]          use pinentry to read the passphrase
   -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
   --version                 print version information
   -x, --expires[=SPEC]      set key expiration [2y]

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 with --time (-t) or --now (-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.

The --check (-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 either case, --repeat (-r) is set to zero unless it was explicitly provided. The additional passphrase check is unnecessary if they Key ID is being checked.

The --protect option uses OpenPGP's S2K feature to encrypt the private key in the exported format. Rather than prompt for an S2K passphrase, passphrase2pgp will reuse your derivation passphrase as the protection passphrase. However, keep in mind that the S2K algorithm is much weaker than the algorithm used to derive the asymmetric key, Argon2id. Given an optional numeric argument, --protect will prompt that many times (like --repeat) for a separate S2K passphrase.

By default keys are not given an expiration date and do not expire. To retire a key, you would need to use another OpenPGP implementation to import your key and generate a revocation certificate. Alternatively, the --expires (-x) option sets an expiration date, defaulting to two years from now. As an optional argument, it accepts a time specification similar to GnuPG: days (d), weeks (w), months (m), and years (y). For example, --expires=10y or -x10y sets the expiration date to 10 years from now. Without a suffix, the value is interpreted as a specific unix epoch timestamp.

Unfortunately there's a bug in the way GnuPG processes key expiration dates that affect passphrase2pgp. Keys with a zero creation date are incorrectly considered never to expire despite an explicit expiration date. This means if you use passphrase2pgp's default creation date, the --expires (-x) may appear not to work, and GnuPG will incorrectly verify signatures from your expired keys. Further, GnuPG generally doesn't compute expiration dates correctly. OpenPGP allows expiration dates beyond the year 2106, and, unlike GnuPG, passphrase2pgp will allow you construct such keys, but GnuPG will use an incorrect (earlier) date.

Examples

Generate a private key and send it to GnuPG (no protection passphrase):

$ passphrase2pgp --uid "..." | gnupg --import

Or, with --protect, reuse the derivation passphrase as the protection passphrase so that the key is encrypted on the GnuPG keyring using your derivation passphrase:

$ passphrase2pgp --protect --uid "..." | gnupg --import

Or to prompt (once) for a different passphrase to use as the protection passphrase:

$ passphrase2pgp --protect=1 --uid "..." | gnupg --import

Create an armored public key for publishing and sharing:

$ passphrase2pgp --uid "..." --armor --public > Real-Name.asc

Since passing --uid every time you need it is tedious, that argument can be supplied implicitly via two environment variables, REALNAME and EMAIL. The remaining examples assume these variables are set.

$ export REALNAME="Real Name"
$ export EMAIL="[email protected]"
$ passphrase2pgp -ap > Real-Name.asc

Create detached signatures (-S) for some files:

$ passphrase2pgp -S document.txt avatar.jpg

This will create document.txt.sig and 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

Normally each command must derive keys from scratch from the passphrase, requiring the user to re-enter it for each command and wait. To avoid this, save the secret key to a file in OpenPGP format and then load (--load) it for other commands. This will save an unprotected version:

$ passphrase2pgp > secret.pgp

Then you can sign files without re-entering your passphrase:

$ passphrase2pgp -S --load secret.pgp document.txt avatar.jpg

If you used an S2K protection passphrase (--protect), passphrase2pgp will prompt for it when loading such keys.

More signatures, but ASCII-armored:

$ passphrase2pgp -S -lsecret.pgp --armor document.txt avatar.jpg
$ gpg --verify document.txt.asc
$ gpg --verify avatar.jpg.asc

Create a cleartext-signed (-T) text document:

$ passphrase2pgp -T >signed-doc.txt <doc.txt

Intended Workflow

There are two usage patterns: "lite" and "full".

When a "lite" user sets up a new computer, they run passphrase2pgp just once and send the key straight into GnuPG. After this they use GnuPG for everything OpenPGP-related. This command installs the secret key in GnuPG with a separate, more convenient, protection passphrase:

$ passphrase2pgp -u '...' -e1 | gpg --import

This user will not need to backup their keyring since they can always regenerate their key in the future. They're also free to destroy their keyring at any moment, such as before their computer is accessed by untrusted people (border agents, etc.).

A "full" user will use passphrase2pgp directly for signatures and will never store the secret key permanently. They derive the key on demand only when needed. To make this convenient, this users sets REALNAME, EMAIL, and KEYID in their .profile. This means they never have to supply --uid, and there's no passphrase confirmation prompt.

For example, supposed John Doe is a "full" user setting up for the first time with the passphrase "boa trusted stew critics dispute asked naming gyms". First he sets his user ID in his .profile:

export REALNAME="John Doe"
export EMAIL="[email protected]"

Then he generates his public key and gets the fingerprint:

$ passphrase2pgp --verbose --public >John-Doe.asc
User ID: John Doe <[email protected]>
passphrase: 
passphrase (repeat): 
Key ID: C8A22A0535AF18BC83D7AE21406CC07F8DABE73B

He publishes John-Doe.asc and adds the fingerprint to his .profile:

export KEYID=C8A22A0535AF18BC83D7AE21406CC07F8DABE73B

This is the actual key for that user ID and passphrase, so you can try each of these commands yourself. Later if he, say, needs to clearsign a message:

$ echo The swallow flies at midnight >message.txt
$ passphrase2pgp -T <message.txt
passphrase: 
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

The swallow flies at midnight
-----BEGIN PGP SIGNATURE-----

wnUEARYIACcFAl1UG1gJEEBswH+Nq+c7FiEEyKIqBTWvGLyD164hQGzAf42r5zsA
ADWqAP9KfoQm02q+AXE5brS9lNZ8LVjFs6CefMA4C/83Da7E4wD/QnYNyFmpmTOm
B6w1UvDnxyD0ksjmyj6NDiRs25b20gk=
=0gf5
-----END PGP SIGNATURE-----

Again, this was all done without ever storing the secret key in the file system, even in protected form.

GnuPG Trust

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. Or use the --trusted-key option in gpg.conf.

$ gpg --edit-key "Real Name"
gpg> trust

Similarly, to allow gpgv to verify your signatures, append your public key to its trusted keyring:

$ passphrase2pgp -p >> ~/.gnupg/trustedkeys.kbx

Signing Git tags and commits

It's even possible to use passphrase2pgp directly to sign your Git tags and commits. Just set gpg.program to passphrase2pgp:

$ git config --global gpg.program passphrase2pgp

When passphrase2pgp detects that it's been invoked via Git, it presents a GnuPG-like interface to Git. When asked to verify tags and commits (git verify-tag, git verify-commit), it delegates to the program named gpg.

OpenSSH format

Despite the name, passphrase2pgp can output a key in OpenSSH format, selected by --format (-f). Passphrase protection (bcrypt) is supported. When using this format, the --armor (-a), --now (-n), --subkey (-s), and --time (-t) options are ignored since they do not apply. The user ID becomes the key comment and is still used as the salt.

$ passphrase2pgp --format ssh | (umask 077; tee id_ed25519)

This will be exactly the same key pair as when generating an OpenPGP key. It's just written out in a different format. The public key will be harmlessly appended to the private key, but it could also be regenerated with ssh-keygen:

$ ssh-keygen -y -f id_ed25519 > id_ed25519.pub

With the --public (-p) option, only the public key will be output.

You may want to add a protection key to the generated key, which, again, can be done with ssh-keygen:

$ ssh-keygen -p -f id_ed25519

Generally you really should have a unique SSH key per host, and this sort of long-term key is both unnecessary and undesired. If you loose access to that computer โ€” theft, retirement, etc. โ€” then you can remove just that host's key as an authorized key without affecting other hosts. In general, SSH keys need not and should not be backed up, including in your brain.

However, there is at least one case where a long-term, important SSH key could be useful. Suppose you have a vital, remote system with password authentication disabled. If you loose access to all of the authorized keys, you can no longer remotely log into that system. Correcting this problem may require traveling to the computer's location or using some inconvenient means to regain access.

Instead, you could use passphrase2pgp to generate an emergency SSH key and install it as an authorized key on the remote host. But don't actually store the private key anywhere and don't normally use this key! When you're in a pinch, use passphrase2pgp to regenerate the emergency key, recover your access, then immediately destroy the emergency key.

Setting up the emergency key ahead of time:

$ passphrase2pgp -u emergency -f ssh -p > ~/.ssh/emergency.pub
$ ssh-copy-id -f -i ~/.ssh/emergency.pub important.example.com

Later, when in dire straits, generate the private key, and use it to install a non-emergency key as a new authorized key:

$ passphrase2pgp -u emergency -f ssh | ssh-add -
$ ssh-copy-id -i ~/.ssh/id_ed25519 important.example.com

Other formats

The --format option also supports:

  • x509: self-signed TLS certificates (--protect unsupported)
  • signify: OpenBSD signify(1) keys, including --protect

Justification

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.

Regarding the encryption subkey

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. However, the upcoming update to OpenPGP, rfc4880bis, adds AEAD encryption, and this could make encryption interesting again.

OpenPGP digital signatures still have some limited use, mostly due to the lack of adoption of the alternatives. The OpenPGP specification is too flexible and is loaded with legacy cruft. Further, GnuPG is honestly not a great OpenPGP implementation, and I do not have high confidence in it.

Roadmap

  • AEAD (tag 20) encryption and limited decryption

References

More Repositories

1

endlessh

SSH tarpit that slowly sends an endless banner
C
7,113
star
2

w64devkit

Portable C and C++ Development Kit for x64 (and x86) Windows
C
2,871
star
3

elfeed

An Emacs web feeds client
Emacs Lisp
1,371
star
4

skewer-mode

Live web development in Emacs
Emacs Lisp
1,066
star
5

hash-prospector

Automated integer hash function discovery
C
672
star
6

enchive

Encrypted personal archives
C
630
star
7

branchless-utf8

Branchless UTF-8 decoder
C
589
star
8

scratch

Personal scratch code
C
366
star
9

pixelcity

Shamus Young's procedural city project
C++
359
star
10

optparse

Portable, reentrant, getopt-like option parser
C
326
star
11

pdjson

C JSON parser library that doesn't suck
C
279
star
12

interactive-c-demo

Demonstration of interactive C programming
C
249
star
13

emacs-aio

async/await for Emacs Lisp
Emacs Lisp
215
star
14

webgl-particles

WebGL particle system demo
JavaScript
203
star
15

fantasyname

Fantasy name generator
C
180
star
16

lstack

C11 Lock-free Stack
C
172
star
17

resurrect-js

JavaScript serialization that preserves behavior and reference circularity.
JavaScript
169
star
18

pure-linux-threads-demo

Pthreads-free Linux threading demo
Assembly
147
star
19

ptrace-examples

Examples for Linux ptrace(2)
C
134
star
20

memdig

Memory cheat tool for Windows and Linux games
C
131
star
21

dosdefender-ld31

DOS Defender (Ludum Dare #31)
C
130
star
22

dotfiles

My personal dotfiles
Shell
124
star
23

Prelude-of-the-Chambered

Notch's Prelude of the Chambered 48-hour game
Java
124
star
24

sort-circle

Colorful sorting animations
C
124
star
25

.emacs.d

My personal .emacs.d
Emacs Lisp
119
star
26

growable-buf

Growable Memory Buffer for C99
C
116
star
27

youtube-dl-emacs

Emacs youtube-dl download manager
Emacs Lisp
104
star
28

getopt

POSIX getopt() as a portable header library
C
102
star
29

trie

C99 trie library
C
98
star
30

opengl-demo

Minimal OpenGL 3.3 core profile demo
C
97
star
31

Minicraft

Notch's Ludum Dare 22 entry.
Java
95
star
32

igloojs

Low-level, fluent, OOP WebGL wrapper
JavaScript
89
star
33

hastyhex

A blazing fast hex dumper
C
88
star
34

webgl-game-of-life

WebGL Game of Life
JavaScript
88
star
35

u-config

a smaller, simpler, portable pkg-config clone
C
84
star
36

bmp

24-bit BMP (Bitmap) ANSI C header library
C
84
star
37

elisp-ffi

Emacs Lisp Foreign Function Interface
C++
83
star
38

rng-js

JavaScript seedable random number generation tools.
JavaScript
82
star
39

mandel-simd

Mandelbrot set in SIMD (SSE, AVX)
C
81
star
40

sample-java-project

Example Ant-based Java project
Java
78
star
41

nasm-mode

Major mode for editing NASM assembly programs
Emacs Lisp
78
star
42

vulkan-test

Test if your system supports Vulkan
C
77
star
43

gap-buffer-animator

Gap buffer animation creator
C
72
star
44

at-el

Prototype-based Emacs Lisp object system
Emacs Lisp
71
star
45

skeeto.github.com

Personal website/blog
HTML
64
star
46

ulid-c

ULID Library for C
C
62
star
47

xf8

8-bit Xor Filter in C99
C
61
star
48

race64

World's fastest Base64 encoder / decoder
C
58
star
49

devdocs-lookup

Quick Emacs API lookup on devdocs.io
Emacs Lisp
58
star
50

webgl-path-solver

WebGL shortest path solver
JavaScript
57
star
51

x86-lookup

Quickly jump to x86 documentation from Emacs
Emacs Lisp
57
star
52

javadoc-lookup

Quickly lookup Javadoc pages from Emacs
Emacs Lisp
55
star
53

am-i-shadowbanned

Online reddit shadowban test
JavaScript
53
star
54

fun-liquid

Physics engine liquid in Java.
Java
53
star
55

minimail

Embeddable POP3 + SMTP server.
C
50
star
56

emacs-memoize

Elisp memoization functions
Emacs Lisp
48
star
57

webgl-fire

WebGL fire effect
JavaScript
47
star
58

simplegpg

Simplified, signify-like interface to GnuPG signatures
Shell
47
star
59

autotetris-mode

Automatically play Emacs Tetris
Emacs Lisp
45
star
60

fiber-await

Win32 Fiber async/await demo
C
45
star
61

lorenz-webgl

Lorenz System WebGL
JavaScript
42
star
62

asteroids-demo

Asteroids Clone for Windows
C
40
star
63

elisp-json-rpc

JSON-RPC library for Emacs Lisp
Emacs Lisp
39
star
64

hashtab

Simple C hash table
C
37
star
65

pgp-poisoner

PGP key poisoner
Go
36
star
66

bf-x86

x86_64 brainfuck compiler
C
36
star
67

wisp

Wisp, a lisp programming language
C
33
star
68

binitools

Bini file translator for the game Freelancer
C
32
star
69

double-pendulum

JavaScript double pendulum simulation with RK4 integration
JavaScript
30
star
70

predd

Multimethods for Emacs Lisp
Emacs Lisp
29
star
71

atomkv

In-memory, JSON, key-value service with compare-and-swap updates and event streams
Go
29
star
72

purgeable

Purgeable memory allocations for Linux
C
28
star
73

lqueue

C11 + Pthreads Atomic Bounded Work Queue
C
28
star
74

uuid

UUID generator for Go
Go
27
star
75

goblin-com

Goblin-COM roguelike game for 7DRL 2015
C
27
star
76

jekyll-deck

Template for Jekyll / deck.js presentations
27
star
77

rlhk

Roguelike Header Kit
C
26
star
78

voronoi-toy

WebGL interactive Voronoi diagram
JavaScript
26
star
79

transcription-mode

Emacs mode for editing transcripts.
Emacs Lisp
25
star
80

october-chess-engine

Java Chess Engine
Java
25
star
81

boids-js

HTML5 boids (skewer-mode demo)
JavaScript
25
star
82

optparse-go

GNU style long options for Go
Go
24
star
83

geohash

Fast, lean, efficient geohash C library
C
24
star
84

bitpack

Emacs Lisp structure packing
Emacs Lisp
23
star
85

lean-static-gpg

Lean, static GnuPG build for Linux
Shell
23
star
86

connect4

Connect Four AI and Engine
C
22
star
87

blowpipe

Authenticated Blowfish-encrypted pipe
C
22
star
88

markov-text

Markov chain text generation in Emacs Lisp
Emacs Lisp
22
star
89

joymacs

Joystick support for Emacs
C
21
star
90

emacs-rsa

RSA cryptography in Emacs Lisp
Emacs Lisp
21
star
91

live-dev-env

A live CD of my personal development environment
Shell
20
star
92

dynamic-function-benchmark

Benchmark for three different kinds of dynamic function calls
C
20
star
93

utf-7

UTF-7 encoder and decoder in ANSI C
C
19
star
94

elisp-fakespace

Emacs Lisp namespaces (defpackage)
Emacs Lisp
18
star
95

siphash

Incremental SipHash in C
C
18
star
96

bencode-c

Bencode decoder in ANSI C
C
17
star
97

british-square

British Square Engine (Analysis and Perfect AI Player)
C
17
star
98

pokerware

Pokerware Secure Passphrase Generation
Makefile
17
star
99

xxtea

100% XXTEA authenticated, chunked file encryption
C
17
star
100

gnupg-windows-build

Cross-compile GnuPG for Windows using Docker
Dockerfile
17
star