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:

  1. Save the script (e.g. as ~/bin/pinentry-auto).
  2. Make it executable (chmod +x pinentry-auto).
  3. Add pinentry-program /path/to/pinentry-auto to ~/.gnupg/gpg-agent.conf.
  4. 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.