Some time ago I needed to write a script which had to ask the user for a password. After a short research it turned out that there are a few options.
The most rudimentary one is probably the “read” built-in command. It has an -s
parameter which does not echo the characters typed. One potential drawback of that is that it stores the read text in an environment variable. This means that if you subsequently use it with a command which is not another built-in, the password might be exposed in the output of ps -ef
on some systems. You could echo
it with redirection to a temporary file (preferably on a tmpfs
in-memory filesystem) and then use the file in subsequent commands, and I’m almost sure this is safe, but please don’t quote me on that – I’m not a security expert, really.
Another interesting option I had no idea existed is the whiptail utility. It is surprisingly versatile – it allows us to get various data from the user (yes/no answers, menu selection, arbitrary text and a few more). It prints the user’s selection on standard output and uses the exit code to signal whether the user chose “OK”/”Yes” or “Cancel”/”No”, which I like better than environment variables. Go to the manual for the details.
Both the above options (and many others, like the dialog command-line utility) have one thing in common – they use the terminal the script is running in to read the password. This is fine in many circumstances, but not always. For example, if I run a script in a terminal, and half a second later this script asks me the password, it’s fine. However, if the script runs for 20 minutes and only asks for the password near the end, that’s much worse – most probably I won’t be just sitting there and looking at the terminal, ready to type a password whenever my machine wants me to do this. (Note that this is not at all unrealistic – for example, a long-running script could need to sudo
something near the end.) Another possible (though possibly a bit contrived) situation is some kind of scheduled job (via cron
, at
or some similar tool). Yet another case (pretty common for us Emacs users) is a script run from within Emacs (like syncing emails with the IMAP server using mbsync
). In some such cases ssh-agent
takes care of getting the password from the user (and one day I’m going to learn how that works…), but some tools do not use it (Borg comes to mind).
Of course, it ssh-agent
can do it, so can I, right? I even remembered the name of the tool ssh-agent
uses under the hood – pinentry
. Could I learn to use it?
The answer, of course, is “yes”. It turns out that you can use pinentry
in a script, although its interface is a bit… strange.
First of all, let me mention that I could not find the “official” docs for pinentry
on the internet, which is weird. The best I could find is some blog post which apparently contains the copy of the official manual. After a while it dawned on me: pinentry
is part of GnuPG, and it contains… a TeXinfo manual!!! I’m a big fan of Info, so I immediately checked my system, and lo and behold – I already have the pinentry
manual in Info!
The second interesting thing is that pinentry
has a few “frontends”. For example, there is pinentry-gtk
and pinentry-qt
– but also pinentry-tty
, pinentry-curses
, and a bit suprisingly, pinentry-emacs
. It is, of course, advisable to use the smallest pinentry
you can – the larger the stack, the larger the potential attack surface. As much as I love Emacs, I have to agree with the pinentry
manual:
Having Emacs get the passphrase is convenient, however, it is a significant security risk. Emacs is a huge program, which doesn’t provide any process isolation to speak of. As such, having it handle the passphrase adds a huge chunk of code to the user’s trusted computing base. Because of this concern, Emacs doesn’t enable this by default, unless the ‘allow-emacs-pinentry’ option is explicitly set in his or her ’.gnupggpg-agent.conf' file./
Unfortunately, even though I enabled the said option, I still wasn’t able to make pinentry-emacs
work, even if just for the sake of an experiment.
The next strange thing about pinentry
is the way you tell it what to do. I am accustomed to CLI tools which accept hundreds of command-line parameters to drive their behavior. It turns out that pinentry
only has about a dozen (including --help
, --version
and --debug
, which you wouldn’t normally use), and I would probably never use more than three of them. How do we use it, then?
Well, the design of pinentry
is unlike most CLI tools. You start it with hardly any CLI parameters, and tell it what to do via its standard input. Conversely, pinentry
’s responds via its standard output. The protocol it uses, called Assuan, is a bit complex, but for simple use cases like using pinentry
to get a password from the user it is simple enough.
For example, this is what you would tell pinentry
to set the “description” (shown above the password input box):
SETDESC Please tell me your secret
Other commands are, for example, SETTITLE
(for the window title), SETPROMPT
, SETOK
and SETCANCEL
(for the buttons), etc. – and most importantly, GETPIN
to actually display the password window and return the typed stuff. The typed password is returned on pinentry
’s standard output prefixed by the capital letter D
and a space, so you need to parse that response (possibly along with OK
’s from every other command like SETPROMPT
) in the program you write to invoke pinentry
. In the case of a shell script, you’d probably use sed
for that:
sed -n 's/^D //p'
Of course, just filtering the output of pinentry
through this won’t necessarily work if the user clicks “Cancel” – in such a case, there will be no line beginning with D
and nothing will be returned, so the script should probably check for that. Also, if the password contains a percent sign, it is represented as %25
(and CR and LF are theoretically represented by %0D
and %0A
respectively, though they probably can’t be entered as a part of the password anyway). This means that you might want to e.g. use another sed
command to take care of percent signs, for example like this:
sed -n -e 's/%25/%/g' -e 's/^D //p'
Also, if you run pinentry
without the normal shell context, where you might not have your usual environment variables, you will probably want to use the --display
option to tell it which X display to use.
I have to admit that I could not make some commands from the docs work – but the most important ones seemed to do what they should. So, from now on, I can make my scripts ask for the password in a much better way than just in the terminal they ran in (if they even ran in a terminal).