GnuPG 2 uses a
pinentry program to prompt
the user for passphrases and PINs. The standard pinentry
collection includes
executables for GNOME, plain GTK+, Qt, Curses, and TTY user interfaces. By
default, the graphical programs will fall back to Curses when $DISPLAY
is
not available. For my own use, I would like the opposite behavior: Present a
text UI if a terminal is available, otherwise fall back to a graphical UI.
This post describes one way to accomplish that behavior.
Pinentry Architecture
gpg-agent
invokes the pinentry executable configured by pinentry-program
in
gpg-agent.conf
(default: pinentry
, which is managed by the Debian
Alternatives System on
Debian-based distros) whenever the user must be prompted for a passphrase
or PIN. The standard input and output of pinentry are pipes over which the
configuration and response information is sent in the Assuan
Protocol. (See the
pinentry
Manual
for specifics.) Additionally, environment variables which contain
configuration information passed via Assuan (e.g. $DISPLAY
, $GPG_TTY
,
$TERM
) are not passed to pinentry. (See
stdenvnames
for a full list and mapping.)
This architecture keeps pinentry simple and self-contained, but it makes environment detection and conditional execution difficult:
-
stdin
is always a pipe. -
$DISPLAY
and$GPG_TTY
are never set. - Reading configuration information requires implementing the Assuan protocol (and proxying it to any child pinentry processes).
- Fallback between different pinentry programs is only possible if they don’t read any Assuan messages before failing (or the messages are proxied to each invocation).
To achieve the desired behavior in a robust way, without additional configuration, subject to the above constraints, likely requires implementing a pinentry program using libassuan or modifying an existing pinentry program to present a UI based on the configuration information passed via Assuan. However, I am too lazy to write and maintain my own pinentry program, so I came up with a different solution which requires a little configuration:
Using $PINENTRY_USER_DATA
for Configuration
As a result of Task 799, GnuPG 2.08 and later
pass the PINENTRY_USER_DATA
environment variable from the calling
environment to gpg-agent to pinentry. The format of this variable is not
specified (and not used by any programs in the standard pinentry collection
that I can find). pinentry-mac
assumes it is a comma-separated sequence of NAME=VALUE pairs with no quoting
or
escaping
and recognizes USE_CURSES=1 to control curses
fallback. I adopted this
convention for a simple pinentry script which chooses the UI based on the
presence of USE_TTY=1
in $PINENTRY_USER_DATA
:
#!/bin/sh
# Choose between pinentry-tty and pinentry-x11 based on whether
# $PINENTRY_USER_DATA contains USE_TTY=1
#
# Based on:
# https://kevinlocke.name/bits/2019/07/31/prefer-terminal-for-gpg-pinentry
#
# Note: Environment detection is difficult.
# - stdin is Assuan pipe, preventing tty checking
# - configuration info (e.g. ttyname) is passed via Assuan pipe, preventing
# parsing or fallback without implementing Assuan protocol.
# - environment is sanitized by atfork_cb in call-pinentry.c (removing $GPG_TTY)
#
# $PINENTRY_USER_DATA is preserved since 2.08 https://dev.gnupg.org/T799
#
# Format of $PINENTRY_USER_DATA not specified (that I can find), pinentry-mac
# assumes comma-separated sequence of NAME=VALUE with no escaping mechanism
# https://github.com/GPGTools/pinentry-mac/blob/v0.9.4/Source/AppDelegate.m#L78
# and recognizes USE_CURSES=1 for curses fallback
# https://github.com/GPGTools/pinentry-mac/pull/2
#
# To the extent possible under law, Kevin Locke <kevin@kevinlocke.name> has
# waived all copyright and related or neighboring rights to this work
# under the terms of CC0: https://creativecommons.org/publicdomain/zero/1.0/
set -Ceu
# Use pinentry-tty if $PINENTRY_USER_DATA contains USE_TTY=1
case "${PINENTRY_USER_DATA-}" in
*USE_TTY=1*)
# Note: Change to pinentry-curses if a Curses UI is preferred.
exec pinentry-tty "$@"
;;
esac
# Otherwise, use any X11 UI (configured by Debian Alternatives System)
# Note: Will fall back to curses if $DISPLAY is not available.
exec pinentry-x11 "$@"
To use this script for pinentry:
- Save the script (e.g. as
~/bin/pinentry-auto
). - Make it executable (
chmod +x pinentry-auto
). - Add
pinentry-program /path/to/pinentry-auto
to~/.gnupg/gpg-agent.conf
. -
export PINENTRY_USER_DATA=USE_TTY=1
in environments where prompting via TTY is desired (e.g. alongside$GPG_TTY
in~/.bashrc
).
The script and settings are also available as a Gist.