[PATCH] Add inside-Emacs mode to GUI pinentry programs
Neal H. Walfield
neal at walfield.org
Wed Jun 3 14:49:27 CEST 2015
Hi Daiki,
I just took a quick look and it seems good. I'll have time to do a
thorough review tomorrow (Thursday).
:) Neal
At Tue, 02 Jun 2015 17:56:44 +0900,
Daiki Ueno wrote:
>
> [1 <text/plain (7bit)>]
> Hello Neal,
>
> Thanks for the thorough review, responses are inlined below.
>
> By the way, on second thoughts, I ended up with a specialized protocol
> for this, to avoid unnecessary character escaping and encoding
> conversion (and also reconnecting to a socket every time). The protocol
> is actually a subset of the Pinentry Assuan protocol, and implemented in
> pinentry.el, which shall be included in Emacs, maybe under lisp/net/ or
> ELPA.
>
> I'm attaching the new patch and pinentry.el. To test, use "M-x
> pinentry-start" instead of "M-x server-start".
>
> "Neal H. Walfield" <neal at walfield.org> writes:
>
> >> +if test "$pinentry_emacs" = "maybe"; then
> >> + AC_MSG_CHECKING([if Unix domain socket is supported])
> >
> > I think we should check for != "no" here. If pinentry_emacs is yes,
> > but we don't support unix domain sockets, then it would be better to
> > error out here than when compiling the code. Do you agree?
>
> Yes, exactly. Fixed.
>
> >> +#include <assert.h>
> >
> > As far as I can tell, this include is gratuitious.
>
> Removed.
>
> >> + if (strlen (tmpdir) + strlen (socket_name) + 10 + 2
> >> + >= sizeof (emacs_server_address.sun_path))
> [...]
> >> + sprintf (emacs_server_address.sun_path, "%s/emacs%lu/%s",
> >> + tmpdir, (unsigned long) uid, socket_name);
> >
> > I think the check above is wrong. It should be something like:
> >
> > if (strlen (tmpdir) + strlen("/emacs") + 10 + strlen("/")
> > + strlen (socket_name) + 1
> > >= sizeof (emacs_server_address.sun_path))
>
> Right, thanks for pointing that out. Fixed using the correct formula.
>
> >> + if (connect (s, (struct sockaddr *) &emacs_server_address,
> >> + strlen (emacs_server_address.sun_path) + 2) < 0)
> >
> > Why don't you just use 'sizeof (emacs_server_address)' here? I really
> > don't like the magic number 2 here.
>
> Fixed using SUN_LEN from GnuPG.
>
> >> + if (strprefix ("-print ", p))
>
> > Does it make sense to emit a warning if there is a line with an
> > unrecognized prefix? As far as I can see, lines with unknown commands
> > are currently ignored.
>
> Yes, added warnings.
>
> >> + sprintf (command, "(" CALLBACK_NAME " \"%s\" %s)",
> >> + name, quoted_value);
> >> + if (quoted_value != nil)
> >> + free (quoted_value);
> >
> > Here's another instance where I'd strongly prefer something like
> > asprintf.
>
> Yes. Those allocation and copying are no longer necessary with the new
> protocol.
>
> >> + quoted_command = build_command (name, local_value);
> >> + if (local_value != value)
> >> + free (local_value);
> >
> > I think this comparison is useless. If value is not NULL, then we
> > call pinentry_utf8_to_local, which always allocates a new buffer.
>
> Good point. Likewise to the above, the extra allocation is not needed
> anymore.
>
> > Does emacs not support setting custom button labels?
>
> Yes, it could. However, for a password query, labels would be too
> verbose to be shown in the minibuffer. So, in pinentry.el, they are
> only used for a confirmation query.
>
> > If pe->prompt is NULL, then you should fallback to pe->default_prompt.
>
> Fixed.
>
> > If you have time, it would be nice to add support for the external
> > password manager checkbox. In the very least, please note the lack of
> > this support with an XXX comment in the source code.
>
> Sure, added a comment.
>
> >> + if (pe->repeat_passphrase)
> >> + pe->repeat_okay = 1;
> >
> > Why are you setting this if the user didn't actually enter the
> > password twice?
>
> I implemented the repeat feature in the new patch.
>
> >> +int
> >> +do_confirm (pinentry_t pe)
> [...]
> >> + set_value (pe, "SETPROMPT", pe->prompt);
> >
> > Again, please fallback to pe->default_prompt.
>
> Fixed.
>
> > Any reason, there is no support for custom button labels? Also, what
> > about support for the other button? If this is not yet possible,
> > please note it with an XXX comment.
>
> Fixed.
>
> >> -#if defined FALLBACK_CURSES || defined PINENTRY_CURSES || defined
> >> PINENTRY_GTK
> >> +#if defined FALLBACK_CURSES || defined INSIDE_EMACS || defined
> >> PINENTRY_CURSES || defined PINENTRY_GTK
> >> char *
> >> pinentry_utf8_to_local (const char *lc_ctype, const char *text)
> >> {
> >
> > I think we should check if PINENTRY_EMACS is defined, not INSIDE_EMACS
> > here.
>
> I reverted this change since the patch no longer uses
> pinentry_utf8_to_local.
>
> Regards,
> --
> Daiki Ueno
> [2 0001-Add-inside-Emacs-mode-to-GUI-pinentry-programs.patch <text/x-patch (7bit)>]
> From 14789d2bfa4fd7931a0191a1379d3fcd449e12aa Mon Sep 17 00:00:00 2001
> From: Daiki Ueno <ueno at gnu.org>
> Date: Wed, 27 May 2015 17:06:08 +0900
> Subject: [PATCH] Add inside-Emacs mode to GUI pinentry programs
>
> * configure.ac: Add --enable-pinentry-emacs and
> --enable-inside-emacs option.
> (BUILD_LIBPINENTRY_EMACS): New conditional.
> (BUILD_PINENTRY_EMACS): New conditional.
> (INSIDE_EMACS): New conditional.
> * Makefile.am (pinentry_emacs): New.
> (SUBDIRS): Add "emacs" subdir if PINENTRY_EMACS is set.
>
> * pinentry/pinentry-emacs.h: New file.
> * pinentry/pinentry-emacs.c: New file.
> * pinentry/Makefile.am: New file.
>
> * emacs/pinentry-emacs.c: New file.
> * emacs/Makefile.am: New file.
>
> * qt4/Makefile.am (libemacs): New variable, set if the
> INSIDE_EMACS conditional is true.
> (pinentry_qt4_LDADD): Add $(libemacs).
> * qt4/main.cpp (main): Set pinentry_cmd_handler if
> the INSIDE_EMACS envvar is set and the socket is usable.
>
> * gnome3/Makefile.am (libemacs): New variable, set if the
> INSIDE_EMACS conditional is true.
> (LDADD): Add $(libemacs).
> * gnome3/pinentry-gnome3.c (main): Set pinentry_cmd_handler if
> the INSIDE_EMACS envvar is set and the socket is usable.
>
> * gtk+-2/Makefile.am (libemacs): New variable, set if the INSIDE_EMACS
> conditional is true.
> (LDADD): Add $(libemacs).
> * gtk+-2/pinentry-gtk-2.c (main): Set pinentry_cmd_handler if
> the INSIDE_EMACS envvar is set and the socket is usable.
> ---
> Makefile.am | 10 +-
> configure.ac | 60 +++++
> emacs/Makefile.am | 29 +++
> emacs/pinentry-emacs.c | 47 ++++
> gnome3/Makefile.am | 8 +-
> gnome3/pinentry-gnome3.c | 21 +-
> gtk+-2/Makefile.am | 8 +-
> gtk+-2/pinentry-gtk-2.c | 25 +-
> pinentry/Makefile.am | 9 +-
> pinentry/pinentry-emacs.c | 640 ++++++++++++++++++++++++++++++++++++++++++++++
> pinentry/pinentry-emacs.h | 41 +++
> qt4/Makefile.am | 9 +-
> qt4/main.cpp | 93 ++++---
> 13 files changed, 941 insertions(+), 59 deletions(-)
> create mode 100644 emacs/Makefile.am
> create mode 100644 emacs/pinentry-emacs.c
> create mode 100644 pinentry/pinentry-emacs.c
> create mode 100644 pinentry/pinentry-emacs.h
>
> diff --git a/Makefile.am b/Makefile.am
> index 177f37e..388464e 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -40,6 +40,12 @@ else
> pinentry_tty =
> endif
>
> +if BUILD_PINENTRY_EMACS
> +pinentry_emacs = emacs
> +else
> +pinentry_emacs =
> +endif
> +
> if BUILD_PINENTRY_GTK_2
> pinentry_gtk_2 = gtk+-2
> else
> @@ -65,8 +71,8 @@ pinentry_w32 =
> endif
>
> SUBDIRS = assuan secmem pinentry ${pinentry_curses} ${pinentry_tty} \
> - ${pinentry_gtk_2} ${pinentry_gnome_3} ${pinentry_qt4} \
> - ${pinentry_w32} doc
> + ${pinentry_emacs} ${pinentry_gtk_2} ${pinentry_gnome_3} \
> + ${pinentry_qt4} ${pinentry_w32} doc
>
>
> install-exec-local:
> diff --git a/configure.ac b/configure.ac
> index 9948d1f..abc248e 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -271,6 +271,63 @@ if test "$pinentry_curses" = "yes" \
> fi
> fi
>
> +dnl
> +dnl Check for emacs pinentry program.
> +dnl
> +AC_ARG_ENABLE(pinentry-emacs,
> + AC_HELP_STRING([--enable-pinentry-emacs], [build emacs pinentry]),
> + pinentry_emacs=$enableval, pinentry_emacs=maybe)
> +AC_ARG_ENABLE(inside-emacs,
> + AC_HELP_STRING([--enable-inside-emacs], [include emacs hack]),
> + inside_emacs=$enableval, inside_emacs=maybe)
> +
> +if test "$pinentry_emacs" != "no"; then
> + AC_MSG_CHECKING([if Unix domain socket is supported])
> + AC_TRY_COMPILE([
> +#include <sys/socket.h>
> +#include <sys/un.h>
> +],
> + [int s = socket (AF_UNIX, SOCK_STREAM, 0);],
> + [_unixsock_works=yes],
> + [_unixsock_works=no])
> + AC_MSG_RESULT($_unixsock_works)
> + if test "$_unixsock_works" = "yes"; then
> + pinentry_emacs=yes
> + else
> + if test "$pinentry_emacs" = "yes"; then
> + AC_MSG_ERROR([[
> +***
> +*** Support for Unix domain sockets is required.
> +***]])
> + fi
> + pinentry_emacs=no
> + fi
> +fi
> +
> +if test "$inside_emacs" = "maybe"; then
> + if test "$pinentry_emacs" = "yes"; then
> + inside_emacs=yes
> + else
> + inside_emacs=no
> + fi
> +fi
> +
> +AM_CONDITIONAL(BUILD_LIBPINENTRY_EMACS,
> + test "$pinentry_emacs" = "yes" -o "$inside_emacs" = "yes")
> +AM_CONDITIONAL(BUILD_PINENTRY_EMACS, test "$pinentry_emacs" = "yes")
> +AM_CONDITIONAL(INSIDE_EMACS, test "$inside_emacs" = "yes")
> +
> +if test "$pinentry_emacs" = "yes"; then
> + AC_DEFINE(PINENTRY_EMACS, 1,
> + [The Emacs version of Pinentry is to be build])
> +fi
> +
> +if test "$inside_emacs" = "yes"; then
> + inside_emacs=yes
> + AC_DEFINE(INSIDE_EMACS, 1,
> + [The GUI pinentries should respect INSIDE_EMACS envvar.])
> +fi
> +
>
>
> dnl
> @@ -512,6 +569,7 @@ secmem/Makefile
> pinentry/Makefile
> curses/Makefile
> tty/Makefile
> +emacs/Makefile
> gtk+-2/Makefile
> gnome3/Makefile
> qt4/Makefile
> @@ -531,12 +589,14 @@ AC_MSG_NOTICE([
>
> Curses Pinentry ..: $pinentry_curses
> TTY Pinentry .....: $pinentry_tty
> + Emacs Pinentry ...: $pinentry_emacs
> GTK+-2 Pinentry ..: $pinentry_gtk_2
> GNOME 3 Pinentry .: $pinentry_gnome_3
> Qt4 Pinentry .....: $pinentry_qt4 $pinentry_qt4_clip_msg
> W32 Pinentry .....: $pinentry_w32
>
> Fallback to Curses: $fallback_curses
> + Inside-EMACS mode : $inside_emacs
>
> libsecret ........: $libsecret
>
> diff --git a/emacs/Makefile.am b/emacs/Makefile.am
> new file mode 100644
> index 0000000..a0bfe6c
> --- /dev/null
> +++ b/emacs/Makefile.am
> @@ -0,0 +1,29 @@
> +# Makefile.am - PIN entry emacs frontend.
> +# Copyright (C) 2002, 2015 g10 Code GmbH
> +#
> +# This file is part of PINENTRY.
> +#
> +# PINENTRY is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# PINENTRY is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
> +
> +## Process this file with automake to produce Makefile.in
> +
> +bin_PROGRAMS = pinentry-emacs
> +
> +AM_CPPFLAGS = $(COMMON_CFLAGS) $(NEMACS_INCLUDE) -I$(top_srcdir)/pinentry
> +LDADD = ../pinentry/libpinentry.a ../pinentry/libpinentry-emacs.a \
> + ../assuan/libassuan.a ../secmem/libsecmem.a \
> + $(COMMON_LIBS) $(LIBCAP) $(LIBEMACS) $(LIBICONV)
> +
> +pinentry_emacs_SOURCES = pinentry-emacs.c
> diff --git a/emacs/pinentry-emacs.c b/emacs/pinentry-emacs.c
> new file mode 100644
> index 0000000..de4ca05
> --- /dev/null
> +++ b/emacs/pinentry-emacs.c
> @@ -0,0 +1,47 @@
> +/* pinentry-emacs.c - A secure emacs dialog for PIN entry, library version
> + Copyright (C) 2015 Daiki Ueno
> +
> + This file is part of PINENTRY.
> +
> + PINENTRY is free software; you can redistribute it and/or modify it
> + under the terms of the GNU General Public License as published by
> + the Free Software Foundation; either version 2 of the License, or
> + (at your option) any later version.
> +
> + PINENTRY is distributed in the hope that it will be useful, but
> + WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + General Public License for more details.
> +
> + You should have received a copy of the GNU General Public License
> + along with this program; if not, see <http://www.gnu.org/licenses/>.
> +*/
> +
> +#ifdef HAVE_CONFIG_H
> +#include <config.h>
> +#endif
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +
> +#include "pinentry.h"
> +#include "pinentry-emacs.h"
> +
> +pinentry_cmd_handler_t pinentry_cmd_handler = emacs_cmd_handler;
> +
> +
> +int
> +main (int argc, char *argv[])
> +{
> + pinentry_init ("pinentry-emacs");
> +
> + if (!pinentry_emacs_init ())
> + return 1;
> +
> + pinentry_parse_opts (argc, argv);
> +
> + if (pinentry_loop ())
> + return 1;
> +
> + return 0;
> +}
> diff --git a/gnome3/Makefile.am b/gnome3/Makefile.am
> index 46639de..c8767df 100644
> --- a/gnome3/Makefile.am
> +++ b/gnome3/Makefile.am
> @@ -29,10 +29,16 @@ ncurses_include =
> libcurses =
> endif
>
> +if INSIDE_EMACS
> +libemacs = ../pinentry/libpinentry-emacs.a
> +else
> +libemacs =
> +endif
> +
> AM_CPPFLAGS = $(COMMON_CFLAGS) $(GNOME3CFLAGS) \
> $(ncurses_include) -I$(top_srcdir)/assuan \
> -I$(top_srcdir)/secmem -I$(top_srcdir)/pinentry
> LDADD = ../pinentry/libpinentry.a ../assuan/libassuan.a ../secmem/libsecmem.a \
> - $(COMMON_LIBS) $(LIBCAP) $(GNOME3LIBS) $(libcurses)
> + $(COMMON_LIBS) $(LIBCAP) $(GNOME3LIBS) $(libcurses) $(libemacs)
>
> pinentry_gnome3_SOURCES = pinentry-gnome3.c
> diff --git a/gnome3/pinentry-gnome3.c b/gnome3/pinentry-gnome3.c
> index 74ec89c..113bde3 100644
> --- a/gnome3/pinentry-gnome3.c
> +++ b/gnome3/pinentry-gnome3.c
> @@ -37,6 +37,10 @@
> #include "pinentry-curses.h"
> #endif
>
> +#ifdef INSIDE_EMACS
> +#include "pinentry-emacs.h"
> +#endif
> +
>
> #define PGMNAME "pinentry-gnome3"
>
> @@ -253,14 +257,21 @@ main (int argc, char *argv[])
> {
> pinentry_init (PGMNAME);
>
> -#ifdef FALLBACK_CURSES
> - if (pinentry_have_display (argc, argv))
> - gtk_init (&argc, &argv);
> +#ifdef INSIDE_EMACS
> + if (pinentry_inside_emacs () && pinentry_emacs_init ())
> + pinentry_cmd_handler = emacs_cmd_handler;
> else
> - pinentry_cmd_handler = curses_cmd_handler;
> +#endif
> + {
> +#ifdef FALLBACK_CURSES
> + if (pinentry_have_display (argc, argv))
> + gtk_init (&argc, &argv);
> + else
> + pinentry_cmd_handler = curses_cmd_handler;
> #else
> - gtk_init (&argc, &argv);
> + gtk_init (&argc, &argv);
> #endif
> + }
>
> pinentry_parse_opts (argc, argv);
>
> diff --git a/gtk+-2/Makefile.am b/gtk+-2/Makefile.am
> index 7e37469..8e7717e 100644
> --- a/gtk+-2/Makefile.am
> +++ b/gtk+-2/Makefile.am
> @@ -29,10 +29,16 @@ ncurses_include =
> libcurses =
> endif
>
> +if INSIDE_EMACS
> +libemacs = ../pinentry/libpinentry-emacs.a $(LIBEMACS) $(LIBICONV)
> +else
> +libemacs =
> +endif
> +
> AM_CPPFLAGS = $(COMMON_CFLAGS) $(GTK2CFLAGS) $(ncurses_include) \
> -I$(top_srcdir)/secmem -I$(top_srcdir)/pinentry
> LDADD = ../pinentry/libpinentry.a ../assuan/libassuan.a ../secmem/libsecmem.a \
> - $(COMMON_LIBS) $(LIBCAP) $(GTK2LIBS) $(libcurses)
> + $(COMMON_LIBS) $(LIBCAP) $(GTK2LIBS) $(libcurses) $(libemacs)
>
> pinentry_gtk_2_SOURCES = pinentry-gtk-2.c \
> gtksecentry.c gtksecentry.h gseal-gtk-compat.h
> diff --git a/gtk+-2/pinentry-gtk-2.c b/gtk+-2/pinentry-gtk-2.c
> index 1a88e5a..7792046 100644
> --- a/gtk+-2/pinentry-gtk-2.c
> +++ b/gtk+-2/pinentry-gtk-2.c
> @@ -52,6 +52,10 @@
> #include "pinentry-curses.h"
> #endif
>
> +#ifdef INSIDE_EMACS
> +#include "pinentry-emacs.h"
> +#endif
> +
>
> #define PGMNAME "pinentry-gtk2"
>
> @@ -716,17 +720,24 @@ main (int argc, char *argv[])
>
> pinentry_init (PGMNAME);
>
> -#ifdef FALLBACK_CURSES
> - if (pinentry_have_display (argc, argv))
> +#ifdef INSIDE_EMACS
> + if (pinentry_inside_emacs () && pinentry_emacs_init ())
> + pinentry_cmd_handler = emacs_cmd_handler;
> + else
> +#endif
> {
> - if (! gtk_init_check (&argc, &argv))
> +#ifdef FALLBACK_CURSES
> + if (pinentry_have_display (argc, argv))
> + {
> + if (! gtk_init_check (&argc, &argv))
> + pinentry_cmd_handler = curses_cmd_handler;
> + }
> + else
> pinentry_cmd_handler = curses_cmd_handler;
> - }
> - else
> - pinentry_cmd_handler = curses_cmd_handler;
> #else
> - gtk_init (&argc, &argv);
> + gtk_init (&argc, &argv);
> #endif
> + }
>
> pinentry_parse_opts (argc, argv);
>
> diff --git a/pinentry/Makefile.am b/pinentry/Makefile.am
> index 7fbbab6..e6e4ba6 100644
> --- a/pinentry/Makefile.am
> +++ b/pinentry/Makefile.am
> @@ -27,7 +27,13 @@ else
> pinentry_curses =
> endif
>
> -noinst_LIBRARIES = libpinentry.a $(pinentry_curses)
> +if BUILD_LIBPINENTRY_EMACS
> +pinentry_emacs = libpinentry-emacs.a
> +else
> +pinentry_emacs =
> +endif
> +
> +noinst_LIBRARIES = libpinentry.a $(pinentry_curses) $(pinentry_emacs)
>
> LDADD = $(COMMON_LIBS)
> AM_CPPFLAGS = $(COMMON_CFLAGS) -I$(top_srcdir)/assuan -I$(top_srcdir)/secmem
> @@ -35,3 +41,4 @@ AM_CPPFLAGS = $(COMMON_CFLAGS) -I$(top_srcdir)/assuan -I$(top_srcdir)/secmem
> libpinentry_a_SOURCES = pinentry.h pinentry.c argparse.c argparse.h \
> password-cache.h password-cache.c
> libpinentry_curses_a_SOURCES = pinentry-curses.h pinentry-curses.c
> +libpinentry_emacs_a_SOURCES = pinentry-emacs.h pinentry-emacs.c
> diff --git a/pinentry/pinentry-emacs.c b/pinentry/pinentry-emacs.c
> new file mode 100644
> index 0000000..280546d
> --- /dev/null
> +++ b/pinentry/pinentry-emacs.c
> @@ -0,0 +1,640 @@
> +/* pinentry-emacs.c - A secure emacs dialog for PIN entry, library version
> + Copyright (C) 2015 Daiki Ueno
> +
> + This file is part of PINENTRY.
> +
> + PINENTRY is free software; you can redistribute it and/or modify it
> + under the terms of the GNU General Public License as published by
> + the Free Software Foundation; either version 2 of the License, or
> + (at your option) any later version.
> +
> + PINENTRY is distributed in the hope that it will be useful, but
> + WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + General Public License for more details.
> +
> + You should have received a copy of the GNU General Public License
> + along with this program; if not, see <http://www.gnu.org/licenses/>.
> +*/
> +
> +#ifdef HAVE_CONFIG_H
> +#include <config.h>
> +#endif
> +#include <signal.h>
> +#include <fcntl.h>
> +#include <unistd.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <errno.h>
> +#include <limits.h>
> +#include <time.h>
> +#include <sys/types.h>
> +#include <sys/stat.h>
> +#include <sys/socket.h>
> +#include <sys/un.h>
> +#ifdef HAVE_UTIME_H
> +#include <utime.h>
> +#endif /*HAVE_UTIME_H*/
> +
> +#include "pinentry.h"
> +#include "assuan.h"
> +#include "memory.h"
> +#include "secmem-util.h"
> +
> +/* The communication mechanism is similar to emacsclient, but there
> + are a few differences:
> +
> + - To avoid unnecessary character escaping and encoding conversion,
> + we use a subset of the Pinentry Assuan protocol, instead of the
> + emacsclient protocol.
> +
> + - We only use a Unix domain socket, while emacsclient has an
> + ability to use a TCP socket. The socket file is located at
> + ${TMPDIR-/tmp}/emacs$UID/pinentry (i.e., under the same directory
> + as the socket file used by emacsclient, so the same permission
> + and file owner settings apply).
> +
> + - The server implementation can be found in pinentry.el, which is
> + available in Emacs 25+ or from ELPA. */
> +
> +#define LINELENGTH ASSUAN_LINELENGTH
> +#define SEND_BUFFER_SIZE 4096
> +#define INITIAL_TIMEOUT 60
> +
> +#define MIN(x, y) ((x) < (y) ? (x) : (y))
> +
> +#ifndef SUN_LEN
> +# define SUN_LEN(ptr) ((size_t) (((struct sockaddr_un *) 0)->sun_path) \
> + + strlen ((ptr)->sun_path))
> +#endif
> +
> +static int emacs_socket = -1;
> +static char send_buffer[SEND_BUFFER_SIZE + 1];
> +static int send_buffer_length; /* Fill pointer for the send buffer. */
> +
> +#ifndef HAVE_DOSISH_SYSTEM
> +static int timed_out;
> +#endif
> +
> +static int
> +set_socket (const char *socket_name)
> +{
> + struct sockaddr_un unaddr;
> + struct stat statbuf;
> + const char *tmpdir;
> + char *tmpdir_storage = NULL;
> + uid_t uid;
> +
> + unaddr.sun_family = AF_UNIX;
> +
> + /* The socket address contains a UID, but POSIX doesn't define the
> + maximum of uid_t:
> + http://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xsh_chap02.html#tag_22_02_12_01
> + We only support 32-bit UIDs, which can be represented with 10
> + decimal digits.
> + */
> + uid = getuid ();
> + if (uid > 0x100000000)
> + {
> + fprintf (stderr, "UID is too large\n");
> + return 0;
> + }
> +
> + tmpdir = getenv ("TMPDIR");
> + if (!tmpdir)
> + {
> +#ifdef _CS_DARWIN_USER_TEMP_DIR
> + size_t n = confstr (_CS_DARWIN_USER_TEMP_DIR, NULL, (size_t) 0);
> + if (n > 0)
> + {
> + tmpdir = tmpdir_storage = malloc (n);
> + if (!tmpdir)
> + {
> + fprintf (stderr, "out of core\n");
> + return 0;
> + }
> + confstr (_CS_DARWIN_USER_TEMP_DIR, tmpdir_storage, n);
> + }
> + else
> +#endif
> + tmpdir = "/tmp";
> + }
> +
> + if (strlen (tmpdir) + strlen ("/emacs") + 10 + strlen ("/")
> + + strlen (socket_name) + 1 >= sizeof (unaddr.sun_path))
> + {
> + fprintf (stderr, "socket name is too long\n");
> + return 0;
> + }
> + sprintf (unaddr.sun_path, "%s/emacs%lu/%s", tmpdir,
> + (unsigned long) uid, socket_name);
> + free (tmpdir_storage);
> +
> + /* See if the socket exists, and if it's owned by us. */
> + if (stat (unaddr.sun_path, &statbuf) == -1)
> + {
> + perror ("stat");
> + return 0;
> + }
> +
> + if (statbuf.st_uid != geteuid ())
> + {
> + fprintf (stderr, "socket is not owned by the same user\n");
> + return 0;
> + }
> +
> + emacs_socket = socket (AF_UNIX, SOCK_STREAM, 0);
> + if (emacs_socket < 0)
> + {
> + perror ("socket");
> + return 0;
> + }
> +
> + if (connect (emacs_socket, (struct sockaddr *) &unaddr,
> + SUN_LEN (&unaddr)) < 0)
> + {
> + perror ("connect");
> + close (emacs_socket);
> + return 0;
> + }
> +
> + return 1;
> +}
> +
> +/* Percent-escape control characters in DATA. Return a newly
> + allocated string, or DATA itself if there is no need to escape any
> + character. Store the length of the string to LENGTHP. */
> +static char *
> +escape (const char *data, size_t *lengthp)
> +{
> + char *buffer, *out_p;
> + size_t length = *lengthp, buffer_length;
> + size_t offset = 0;
> + size_t count = 0;
> +
> + while (offset < length)
> + {
> + switch (data[offset])
> + {
> + case '%': case '\n': case '\r':
> + count++;
> + break;
> + default:
> + break;
> + }
> + offset++;
> + }
> +
> + if (count == 0)
> + return (char *) data;
> +
> + buffer_length = length + count * 2;
> + buffer = malloc (buffer_length);
> + if (!buffer)
> + return NULL;
> +
> + out_p = buffer;
> + while (offset < length)
> + {
> + int c = data[offset];
> + switch (c)
> + {
> + case '%': case '\n': case '\r':
> + sprintf (out_p, "%%%02X", c);
> + out_p += 3;
> + break;
> + default:
> + *out_p++ = c;
> + break;
> + }
> + offset++;
> + }
> +
> + *lengthp = buffer_length;
> + return buffer;
> +}
> +
> +/* The inverse of escape. Removes quoting in string STR by modifying
> + the string in place. */
> +static char *
> +unescape (char *data, size_t *lengthp)
> +{
> + char *p = data, *q = data;
> +
> + while (*p)
> + {
> + if (*p == '%' && p[1] && p[2])
> + {
> + p++;
> + *q++ = xtoi_2 (p);
> + p += 2;
> + }
> + else
> + *q++ = *p++;
> + }
> + *q = 0;
> + *lengthp = q - data;
> + return data;
> +}
> +
> +/* Let's send the data to Emacs when either
> + - the data ends in "\n", or
> + - the buffer is full (but this shouldn't happen)
> + Otherwise, we just accumulate it. */
> +static int
> +send_to_emacs (int s, const char *buffer, size_t length)
> +{
> + int retval = 1;
> +
> + while (*buffer)
> + {
> + size_t part = MIN (length, SEND_BUFFER_SIZE - send_buffer_length);
> + memcpy (&send_buffer[send_buffer_length], buffer, part);
> + buffer += part;
> + send_buffer_length += part;
> +
> + if (send_buffer_length == SEND_BUFFER_SIZE
> + || (send_buffer_length > 0
> + && send_buffer[send_buffer_length-1] == '\n'))
> + {
> + int sent = send (s, send_buffer, send_buffer_length, 0);
> + if (sent < 0)
> + {
> + fprintf (stderr, "failed to send %d bytes to socket: %s\n",
> + send_buffer_length, strerror (errno));
> + retval = 0;
> + break;
> + }
> + if (sent != send_buffer_length)
> + memmove (send_buffer, &send_buffer[sent],
> + send_buffer_length - sent);
> + send_buffer_length -= sent;
> + }
> +
> + length -= part;
> + }
> +
> + return retval;
> +}
> +
> +/* Read a server response. If the response contains any data, this
> + function returns it as a string. If BUFFER is not NULL, the data
> + will be stored there. Otherwise, it allocates memory. */
> +static char *
> +read_from_emacs (int s, int timeout, char *buffer, size_t *lengthp,
> + assuan_error_t *error)
> +{
> + struct timeval tv;
> + fd_set rfds;
> + int retval;
> + size_t offset = 0;
> + int allocated = buffer == NULL;
> + size_t allocated_capacity = 0;
> + int got_response = 0;
> + char read_buffer[BUFSIZ + 1];
> + size_t read_offset = 0;
> +
> + tv.tv_sec = timeout;
> + tv.tv_usec = 0;
> +
> + FD_ZERO (&rfds);
> + FD_SET (s, &rfds);
> + retval = select (s + 1, &rfds, NULL, NULL, &tv);
> + if (retval == -1)
> + {
> + perror ("select");
> + return 0;
> + }
> + else if (retval == 0)
> + {
> + timed_out = 1;
> + return 0;
> + }
> +
> + while (!got_response)
> + {
> + int rl = 0;
> + char *p, *end_p;
> + do
> + {
> + errno = 0;
> + rl = recv (s, read_buffer + read_offset,
> + sizeof (read_buffer) - 1 - read_offset, 0);
> + }
> + /* If we receive a signal (e.g. SIGWINCH, which we pass
> + through to Emacs), on some OSes we get EINTR and must retry. */
> + while (rl < 0 && errno == EINTR);
> +
> + if (rl < 0)
> + {
> + perror ("recv");
> + *error = ASSUAN_General_Error;
> + goto error;
> + }
> + if (rl == 0)
> + break;
> +
> + read_offset += rl;
> + read_buffer[read_offset] = '\0';
> +
> + end_p = strchr (read_buffer, '\n');
> +
> + /* If the buffer is filled without NL, throw the contents away.
> + FIXME: We could return ASSUAN_Line_Too_Long or
> + ASSUAN_Line_Not_Terminated here. */
> + if (!end_p && read_offset == sizeof (read_buffer) - 1)
> + read_offset = 0;
> +
> + /* Loop over all NL-terminated messages. */
> + for (p = read_buffer; end_p; p = end_p + 1, end_p = strchr (p, '\n'))
> + {
> + *end_p = '\0';
> + if (!strncmp ("D ", p, 2))
> + {
> + char *data;
> + size_t data_length;
> +
> + data = p + 2;
> + data_length = end_p - data;
> +
> + if (allocated && offset + data_length > allocated_capacity)
> + {
> + allocated_capacity = allocated_capacity * 2 + 10;
> + buffer = realloc (buffer, allocated_capacity);
> + if (!buffer)
> + {
> + *error = ASSUAN_Out_Of_Core;
> + goto error;
> + }
> + }
> + else if (!allocated && offset + data_length > *lengthp)
> + {
> + *error = ASSUAN_General_Error;
> + goto error;
> + }
> +
> + memcpy (&buffer[offset], data, data_length);
> + offset += data_length;
> + }
> + else if (!strcmp ("OK", p) || !strncmp ("OK ", p, 3))
> + {
> + *error = ASSUAN_No_Error;
> + got_response = 1;
> + break;
> + }
> + else if (!strncmp ("ERR ", p, 4))
> + {
> + unsigned long code = strtoul (p + 4, NULL, 10);
> + if (code == ULONG_MAX && errno == ERANGE)
> + *error = ASSUAN_General_Error;
> + else
> + *error = code;
> + got_response = 1;
> + break;
> + }
> + else if (!strncmp ("# ", p, 2))
> + ;
> + else
> + fprintf (stderr, "invalid response: %s\n", p);
> + }
> +
> + if (!got_response)
> + {
> + size_t length = &read_buffer[read_offset] - p;
> + memmove (read_buffer, p, length);
> + read_offset = length;
> + }
> + }
> +
> + if (*error == ASSUAN_No_Error)
> + {
> + *lengthp = offset;
> + return buffer;
> + }
> +
> + error:
> + if (allocated)
> + free (buffer);
> +
> + return NULL;
> +}
> +
> +int
> +set_label (pinentry_t pe, const char *name, const char *value)
> +{
> + char buffer[16], *escaped;
> + size_t length;
> + assuan_error_t error;
> + int retval;
> +
> + if (!send_to_emacs (emacs_socket, name, strlen (name))
> + || !send_to_emacs (emacs_socket, " ", 1))
> + return 0;
> +
> + length = strlen (value);
> + escaped = escape (value, &length);
> + if (!escaped)
> + return 0;
> +
> + retval = send_to_emacs (emacs_socket, escaped, length)
> + && send_to_emacs (emacs_socket, "\n", 1);
> +
> + if (escaped != value)
> + free (escaped);
> + if (!retval)
> + return 0;
> +
> + length = sizeof (buffer);
> + read_from_emacs (emacs_socket, pe->timeout, buffer, &length, &error);
> + return error == ASSUAN_No_Error;
> +}
> +
> +static void
> +set_labels (pinentry_t pe)
> +{
> + if (pe->title)
> + set_label (pe, "SETTITLE", pe->title);
> + if (pe->description)
> + set_label (pe, "SETDESC", pe->description);
> + if (pe->error)
> + set_label (pe, "SETERROR", pe->error);
> + if (pe->prompt)
> + set_label (pe, "SETPROMPT", pe->prompt);
> + else if (pe->default_prompt)
> + set_label (pe, "SETPROMPT", pe->default_prompt);
> + if (pe->repeat_passphrase)
> + set_label (pe, "SETREPEAT", pe->repeat_passphrase);
> + if (pe->repeat_error_string)
> + set_label (pe, "SETREPEATERROR", pe->repeat_error_string);
> +
> + /* XXX: pe->quality_bar and pe->quality_bar_tt are not supported. */
> +
> + /* Buttons. */
> + if (pe->ok)
> + set_label (pe, "SETOK", pe->ok);
> + else if (pe->default_ok)
> + set_label (pe, "SETOK", pe->default_ok);
> + if (pe->cancel)
> + set_label (pe, "SETCANCEL", pe->cancel);
> + else if (pe->default_ok)
> + set_label (pe, "SETCANCEL", pe->default_cancel);
> + if (pe->notok)
> + set_label (pe, "SETNOTOK", pe->notok);
> +}
> +
> +static int
> +do_password (pinentry_t pe)
> +{
> + char *escaped, *password;
> + size_t length;
> + assuan_error_t error;
> +
> + set_labels (pe);
> +
> + if (!send_to_emacs (emacs_socket, "GETPIN\n", 7))
> + return -1;
> +
> + escaped = read_from_emacs (emacs_socket, pe->timeout, NULL, &length, &error);
> + if (error != ASSUAN_No_Error)
> + {
> + pe->specific_err = error;
> + return -1;
> + }
> + /* No data sent from the server - maybe cancelled. */
> + if (!escaped)
> + {
> + pe->specific_err = ASSUAN_General_Error;
> + return -1;
> + }
> +
> + password = unescape (escaped, &length);
> + pinentry_setbufferlen (pe, length);
> + if (pe->pin)
> + memcpy (pe->pin, password, length);
> +
> + if (pe->repeat_passphrase)
> + pe->repeat_okay = 1;
> +
> + if (password != escaped)
> + free (password);
> + free (escaped);
> +
> + /* XXX: we don't support external password cache (yet). */
> +
> + return 1;
> +}
> +
> +static int
> +do_confirm (pinentry_t pe)
> +{
> + char buffer[16];
> + size_t length;
> + assuan_error_t error;
> +
> + set_labels (pe);
> +
> + if (!send_to_emacs (emacs_socket, "CONFIRM\n", 8))
> + return 0;
> +
> + length = sizeof (buffer);
> + read_from_emacs (emacs_socket, pe->timeout, buffer, &length, &error);
> + if (error != ASSUAN_No_Error)
> + {
> + pe->specific_err = error;
> + return 0;
> + }
> +
> + return 1;
> +}
> +
> +/* If a touch has been registered, touch that file. */
> +static void
> +do_touch_file (pinentry_t pinentry)
> +{
> +#ifdef HAVE_UTIME_H
> + struct stat st;
> + time_t tim;
> +
> + if (!pinentry->touch_file || !*pinentry->touch_file)
> + return;
> +
> + if (stat (pinentry->touch_file, &st))
> + return; /* Oops. */
> +
> + /* Make sure that we actually update the mtime. */
> + while ( (tim = time (NULL)) == st.st_mtime )
> + sleep (1);
> +
> + /* Update but ignore errors as we can't do anything in that case.
> + Printing error messages may even clubber the display further. */
> + utime (pinentry->touch_file, NULL);
> +#endif /*HAVE_UTIME_H*/
> +}
> +
> +#ifndef HAVE_DOSISH_SYSTEM
> +static void
> +catchsig (int sig)
> +{
> + if (sig == SIGALRM)
> + timed_out = 1;
> +}
> +#endif
> +
> +int
> +emacs_cmd_handler (pinentry_t pe)
> +{
> + int rc;
> +
> +#ifndef HAVE_DOSISH_SYSTEM
> + timed_out = 0;
> +
> + if (pe->timeout)
> + {
> + struct sigaction sa;
> +
> + memset (&sa, 0, sizeof(sa));
> + sa.sa_handler = catchsig;
> + sigaction (SIGALRM, &sa, NULL);
> + alarm (pe->timeout);
> + }
> +#endif
> +
> + if (pe->pin)
> + rc = do_password (pe);
> + else
> + rc = do_confirm (pe);
> +
> + do_touch_file (pe);
> + return rc;
> +}
> +
> +int
> +pinentry_inside_emacs (void)
> +{
> + const char *envvar;
> +
> + /* Check if INSIDE_EMACS envvar is set. */
> + envvar = getenv ("INSIDE_EMACS");
> + if (!envvar || !*envvar)
> + return 0;
> +
> + /* FIXME: Additional checks for the value. */
> + return 1;
> +}
> +
> +int
> +pinentry_emacs_init (void)
> +{
> + char buffer[256];
> + size_t length = sizeof (buffer);
> + assuan_error_t error;
> +
> + /* Check if we can connect to the Emacs server socket. */
> + if (!set_socket ("pinentry"))
> + return 0;
> +
> + /* Check if the server responds. */
> + read_from_emacs (emacs_socket, INITIAL_TIMEOUT, buffer, &length, &error);
> + return error == ASSUAN_No_Error;
> +}
> diff --git a/pinentry/pinentry-emacs.h b/pinentry/pinentry-emacs.h
> new file mode 100644
> index 0000000..732c0ac
> --- /dev/null
> +++ b/pinentry/pinentry-emacs.h
> @@ -0,0 +1,41 @@
> +/* pinentry-emacs.c - A secure emacs dialog for PIN entry, library version
> + Copyright (C) 2015 Daiki Ueno
> +
> + This file is part of PINENTRY.
> +
> + PINENTRY is free software; you can redistribute it and/or modify it
> + under the terms of the GNU General Public License as published by
> + the Free Software Foundation; either version 2 of the License, or
> + (at your option) any later version.
> +
> + PINENTRY is distributed in the hope that it will be useful, but
> + WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + General Public License for more details.
> +
> + You should have received a copy of the GNU General Public License
> + along with this program; if not, see <http://www.gnu.org/licenses/>.
> +*/
> +
> +#ifndef PINENTRY_EMACS_H
> +#define PINENTRY_EMACS_H
> +
> +#include "pinentry.h"
> +
> +#ifdef __cplusplus
> +extern "C" {
> +#endif
> +
> +/* Return true if INSIDE_EMACS is set. */
> +int pinentry_inside_emacs (void);
> +
> +/* Initialize the Emacs interface, return true if success. */
> +int pinentry_emacs_init (void);
> +
> +int emacs_cmd_handler (pinentry_t pinentry);
> +
> +#ifdef __cplusplus
> +}
> +#endif
> +
> +#endif /* PINENTRY_EMACS_H */
> diff --git a/qt4/Makefile.am b/qt4/Makefile.am
> index 816aade..c2ee3c6 100644
> --- a/qt4/Makefile.am
> +++ b/qt4/Makefile.am
> @@ -33,6 +33,12 @@ ncurses_include =
> libcurses =
> endif
>
> +if INSIDE_EMACS
> +libemacs = ../pinentry/libpinentry-emacs.a $(LIBEMACS) $(LIBICONV)
> +else
> +libemacs =
> +endif
> +
>
> AM_CPPFLAGS = $(COMMON_CFLAGS) \
> -I$(top_srcdir) -I$(top_srcdir)/assuan -I$(top_srcdir)/secmem \
> @@ -41,7 +47,8 @@ AM_CXXFLAGS = $(QT4_CORE_CFLAGS) $(QT4_GUI_CFLAGS)
> pinentry_qt4_LDADD = \
> ../pinentry/libpinentry.a $(top_builddir)/assuan/libassuan.a \
> $(top_builddir)/secmem/libsecmem.a \
> - $(COMMON_LIBS) $(QT4_CORE_LIBS) $(QT4_GUI_LIBS) $(libcurses) $(LIBCAP)
> + $(COMMON_LIBS) $(QT4_CORE_LIBS) $(QT4_GUI_LIBS) \
> + $(libcurses) $(libemacs) $(LIBCAP)
>
> BUILT_SOURCES = \
> pinentryconfirm.moc qsecurelineedit.moc pinentrydialog.moc
> diff --git a/qt4/main.cpp b/qt4/main.cpp
> index 37b6e7b..bb682b3 100644
> --- a/qt4/main.cpp
> +++ b/qt4/main.cpp
> @@ -50,6 +50,10 @@
> #include <pinentry-curses.h>
> #endif
>
> +#ifdef INSIDE_EMACS
> +#include "pinentry-emacs.h"
> +#endif
> +
> static QString escape_accel( const QString & s ) {
>
> QString result;
> @@ -263,51 +267,58 @@ main (int argc, char *argv[])
>
> std::auto_ptr<QApplication> app;
>
> -#ifdef FALLBACK_CURSES
> - if (!pinentry_have_display (argc, argv))
> - pinentry_cmd_handler = curses_cmd_handler;
> +#ifdef INSIDE_EMACS
> + if (pinentry_inside_emacs () && pinentry_emacs_init ())
> + pinentry_cmd_handler = emacs_cmd_handler;
> else
> #endif
> {
> - /* Qt does only understand -display but not --display; thus we
> - are fixing that here. The code is pretty simply and may get
> - confused if an argument is called "--display". */
> - char **new_argv, *p;
> - size_t n;
> - int i, done;
> -
> - for (n=0,i=0; i < argc; i++)
> - n += strlen (argv[i])+1;
> - n++;
> - new_argv = (char**)calloc (argc+1, sizeof *new_argv);
> - if (new_argv)
> - *new_argv = (char*)malloc (n);
> - if (!new_argv || !*new_argv)
> - {
> - fprintf (stderr, "pinentry-qt4: can't fixup argument list: %s\n",
> - strerror (errno));
> - exit (EXIT_FAILURE);
> -
> - }
> - for (done=0,p=*new_argv,i=0; i < argc; i++)
> - if (!done && !strcmp (argv[i], "--display"))
> - {
> - new_argv[i] = strcpy (p, argv[i]+1);
> - p += strlen (argv[i]+1) + 1;
> - done = 1;
> - }
> - else
> - {
> - new_argv[i] = strcpy (p, argv[i]);
> - p += strlen (argv[i]) + 1;
> - }
> +#ifdef FALLBACK_CURSES
> + if (!pinentry_have_display (argc, argv))
> + pinentry_cmd_handler = curses_cmd_handler;
> + else
> +#endif
> + {
> + /* Qt does only understand -display but not --display; thus we
> + are fixing that here. The code is pretty simply and may get
> + confused if an argument is called "--display". */
> + char **new_argv, *p;
> + size_t n;
> + int i, done;
> +
> + for (n=0,i=0; i < argc; i++)
> + n += strlen (argv[i])+1;
> + n++;
> + new_argv = (char**)calloc (argc+1, sizeof *new_argv);
> + if (new_argv)
> + *new_argv = (char*)malloc (n);
> + if (!new_argv || !*new_argv)
> + {
> + fprintf (stderr, "pinentry-qt4: can't fixup argument list: %s\n",
> + strerror (errno));
> + exit (EXIT_FAILURE);
>
> - /* We use a modal dialog window, so we don't need the application
> - window anymore. */
> - i = argc;
> - app.reset (new QApplication (i, new_argv));
> - const QIcon icon( QLatin1String( ":/document-encrypt.png" ) );
> - app->setWindowIcon( icon );
> + }
> + for (done=0,p=*new_argv,i=0; i < argc; i++)
> + if (!done && !strcmp (argv[i], "--display"))
> + {
> + new_argv[i] = strcpy (p, argv[i]+1);
> + p += strlen (argv[i]+1) + 1;
> + done = 1;
> + }
> + else
> + {
> + new_argv[i] = strcpy (p, argv[i]);
> + p += strlen (argv[i]) + 1;
> + }
> +
> + /* We use a modal dialog window, so we don't need the application
> + window anymore. */
> + i = argc;
> + app.reset (new QApplication (i, new_argv));
> + const QIcon icon( QLatin1String( ":/document-encrypt.png" ) );
> + app->setWindowIcon( icon );
> + }
> }
>
>
> --
> 2.1.0
>
> [3 pinentry.el <text/plain (7bit)>]
> ;;; pinentry.el --- GnuPG Pinentry server implementation -*- lexical-binding: t -*-
>
> ;; Copyright (C) 2015 Free Software Foundation, Inc.
>
> ;; Author: Daiki Ueno <ueno at gnu.org>
> ;; Keywords: GnuPG
>
> ;; This file is part of GNU Emacs.
>
> ;; GNU Emacs is free software: you can redistribute it and/or modify
> ;; it under the terms of the GNU General Public License as published by
> ;; the Free Software Foundation, either version 3 of the License, or
> ;; (at your option) any later version.
>
> ;; GNU Emacs is distributed in the hope that it will be useful,
> ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
> ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> ;; GNU General Public License for more details.
>
> ;; You should have received a copy of the GNU General Public License
> ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
>
> ;;; Code:
>
> (defvar pinentry--server-process nil)
> (defvar pinentry--connection-process-list nil)
>
> (defvar pinentry--labels nil)
> (put 'pinentry-read-point 'permanent-local t)
> (defvar pinentry--read-point nil)
> (put 'pinentry--read-point 'permanent-local t)
>
> ;; We use the same location as `server-socket-dir', when local sockets
> ;; are supported.
> (defvar pinentry--socket-dir
> (format "%s/emacs%d" (or (getenv "TMPDIR") "/tmp") (user-uid))
> "The directory in which to place the server socket.
> If local sockets are not supported, this is nil.")
>
> (defconst pinentry--set-label-commands
> '("SETPROMPT" "SETTITLE" "SETDESC"
> "SETREPEAT" "SETREPEATERROR"
> "SETOK" "SETCANCEL" "SETNOTOK"))
>
> (defmacro pinentry--error-code (code)
> (logior (lsh 5 24) code))
> (defconst pinentry--error-not-implemented
> (cons (pinentry--error-code 69) "not implemented"))
> (defconst pinentry--error-cancelled
> (cons (pinentry--error-code 99) "cancelled"))
> (defconst pinentry--error-not-confirmed
> (cons (pinentry--error-code 114) "not confirmed"))
>
> (autoload 'server-ensure-safe-dir "server")
>
> ;;;###autoload
> (defun pinentry-start ()
> "Start a Pinentry service."
> (interactive)
> (unless (featurep 'make-network-process '(:family local))
> (error "local sockets are not supported"))
> (if (process-live-p pinentry--server-process)
> (message "Pinentry service is already running")
> (let* ((server-file (expand-file-name "pinentry" pinentry--socket-dir)))
> (server-ensure-safe-dir pinentry--socket-dir)
> ;; Delete the socket files made by previous server invocations.
> (ignore-errors
> (let (delete-by-moving-to-trash)
> (delete-file server-file)))
> (setq pinentry--server-process
> (make-network-process
> :name "pinentry"
> :server t
> :noquery t
> :sentinel #'pinentry--process-sentinel
> :filter #'pinentry--process-filter
> :coding 'no-conversion
> :family 'local
> :service server-file))
> (process-put pinentry--server-process :server-file server-file))))
>
> (defun pinentry-stop ()
> "Stop a Pinentry service."
> (interactive)
> (when (process-live-p pinentry--server-process)
> (delete-process pinentry--server-process))
> (setq pinentry--server-process nil)
> (dolist (process pinentry--connection-process-list)
> (when (buffer-live-p (process-buffer process))
> (kill-buffer (process-buffer process))))
> (setq pinentry--connection-process-list nil))
>
> (defun pinentry--labels-to-shortcuts (labels)
> (mapcar (lambda (label)
> (when label
> (if (string-match "_\\([[:alnum:]]\\)" label)
> (let* ((key (match-string 1 label))
> (c (downcase (aref key 0))))
> (setq label (replace-match
> (propertize key 'face 'underline)
> t t label))
> (cons c label))
> (cons (if (= (length label) 0)
> ??
> (downcase (aref 0 label)))
> label))))
> labels))
>
> (defun pinentry--escape-string (string)
> "Escape STRING in the Assuan percent escape."
> (let ((length (length string))
> (index 0)
> (count 0))
> (while (< index length)
> (if (memq (aref string index) '(?\n ?\r ?%))
> (setq count (1+ count)))
> (setq index (1+ index)))
> (setq index 0)
> (let ((result (make-string (+ length (* count 2)) ?\0))
> (result-index 0)
> c)
> (while (< index length)
> (setq c (aref string index))
> (if (memq c '(?\n ?\r ?%))
> (let ((hex (format "%02X" c)))
> (aset result result-index ?%)
> (setq result-index (1+ result-index))
> (aset result result-index (aref hex 0))
> (setq result-index (1+ result-index))
> (aset result result-index (aref hex 1))
> (setq result-index (1+ result-index)))
> (aset result result-index c)
> (setq result-index (1+ result-index)))
> (setq index (1+ index)))
> result)))
>
> (defun pinentry--send-data (process escaped)
> "Send a string ESCAPED to a process PROCESS.
> ESCAPED will be split if it exceeds the line length limit of the
> Assuan protocol."
> (let ((length (length escaped))
> (index 0))
> (if (= length 0)
> (process-send-string process "D \n")
> (while (< index length)
> ;; 997 = ASSUAN_LINELENGTH (= 1000) - strlen ("D \n")
> (let* ((sub-length (min (- length index) 997))
> (sub (substring escaped index (+ index sub-length))))
> (unwind-protect
> (progn
> (process-send-string process "D ")
> (process-send-string process sub)
> (process-send-string process "\n"))
> (clear-string sub))
> (setq index (+ index sub-length)))))))
>
> (defun pinentry--send-error (process error)
> (process-send-string process (format "ERR %d %s\n" (car error) (cdr error))))
>
> (defun pinentry--process-filter (process input)
> (unless (buffer-live-p (process-buffer process))
> (let ((buffer (generate-new-buffer " *pinentry*")))
> (set-process-buffer process buffer)
> (with-current-buffer buffer
> (if (fboundp 'set-buffer-multibyte)
> (set-buffer-multibyte nil))
> (make-local-variable 'pinentry--read-point)
> (setq pinentry--read-point (point-min))
> (make-local-variable 'pinentry--labels))))
> (with-current-buffer (process-buffer process)
> (save-excursion
> (goto-char (point-max))
> (insert input)
> (goto-char pinentry--read-point)
> (beginning-of-line)
> (while (looking-at ".*\n") ;the input line finished
> (if (looking-at "\\([A-Z_]+\\) ?\\(.*\\)")
> (let ((command (match-string 1))
> (string (match-string 2)))
> (pcase command
> ((and set (guard (member set pinentry--set-label-commands)))
> (when (> (length string) 0)
> (let* ((symbol (intern (downcase (substring set 3))))
> (entry (assq symbol pinentry--labels))
> (label (decode-coding-string string 'utf-8)))
> (if entry
> (setcdr entry label)
> (push (cons symbol label) pinentry--labels))))
> (process-send-string process "OK\n"))
> ("NOP"
> (process-send-string process "OK\n"))
> ("GETPIN"
> (let ((prompt
> (or (cdr (assq 'desc pinentry--labels))
> (cdr (assq 'prompt pinentry--labels))
> ""))
> (confirm (not (null (assq 'repeat pinentry--labels))))
> entry)
> (if (setq entry (assq 'error pinentry--labels))
> (setq prompt (concat "Error: "
> (propertize
> (copy-sequence (cdr entry))
> 'face 'error)
> "\n"
> prompt)))
> (if (setq entry (assq 'title pinentry--labels))
> (setq prompt (format "[%s] %s"
> (cdr entry) prompt)))
> (if (string-match ":?[ \n]*\\'" prompt)
> (setq prompt (concat
> (substring
> prompt 0 (match-beginning 0)) ": ")))
> (let (passphrase escaped-passphrase encoded-passphrase)
> (unwind-protect
> (condition-case nil
> (progn
> (setq passphrase
> (read-passwd prompt confirm))
> (setq escaped-passphrase
> (pinentry--escape-string
> passphrase))
> (setq encoded-passphrase (encode-coding-string
> escaped-passphrase
> 'utf-8))
> (pinentry--send-data
> process encoded-passphrase)
> (process-send-string process "OK\n"))
> (error
> (pinentry--send-error
> process
> pinentry--error-cancelled)))
> (if passphrase
> (clear-string passphrase))
> (if escaped-passphrase
> (clear-string escaped-passphrase))
> (if encoded-passphrase
> (clear-string encoded-passphrase))))
> (setq pinentry--labels nil)))
> ("CONFIRM"
> (let ((prompt
> (or (cdr (assq 'desc pinentry--labels))
> ""))
> (buttons
> (pinentry--labels-to-shortcuts
> (list (cdr (assq 'ok pinentry--labels))
> (cdr (assq 'notok pinentry--labels))
> (cdr (assq 'cancel pinentry--labels)))))
> entry)
> (if (setq entry (assq 'error pinentry--labels))
> (setq prompt (concat "Error: "
> (propertize
> (copy-sequence (cdr entry))
> 'face 'error)
> "\n"
> prompt)))
> (if (setq entry (assq 'title pinentry--labels))
> (setq prompt (format "[%s] %s"
> (cdr entry) prompt)))
> (if (remq nil buttons)
> (progn
> (setq prompt
> (concat prompt " ("
> (mapconcat #'cdr (remq nil buttons)
> ", ")
> ") "))
> (condition-case nil
> (let ((result (read-char prompt)))
> (if (eq result (caar buttons))
> (process-send-string process "OK\n")
> (if (eq result (car (nth 1 buttons)))
> (pinentry--send-error
> process
> pinentry--error-not-confirmed)
> (pinentry--send-error
> process
> pinentry--error-cancelled))))
> (error
> (pinentry--send-error
> process
> pinentry--error-cancelled))))
> (if (string-match "[ \n]*\\'" prompt)
> (setq prompt (concat
> (substring
> prompt 0 (match-beginning 0)) " ")))
> (if (condition-case nil
> (y-or-n-p prompt)
> (quit))
> (process-send-string process "OK\n")
> (pinentry--send-error
> process
> pinentry--error-not-confirmed)))
> (setq pinentry--labels nil)))
> (_ (pinentry--send-error
> process
> pinentry--error-not-implemented)))
> (forward-line)
> (setq pinentry--read-point (point))))))))
>
> (defun pinentry--process-sentinel (process _status)
> "The process sentinel for Emacs server connections."
> ;; If this is a new client process, set the query-on-exit flag to nil
> ;; for this process (it isn't inherited from the server process).
> (when (and (eq (process-status process) 'open)
> (process-query-on-exit-flag process))
> (push process pinentry--connection-process-list)
> (set-process-query-on-exit-flag process nil)
> (process-send-string process "OK Your orders please\n"))
> ;; Kill the process buffer of the connection process.
> (when (and (not (process-contact process :server))
> (eq (process-status process) 'closed))
> (when (buffer-live-p (process-buffer process))
> (kill-buffer (process-buffer process)))
> (setq pinentry--connection-process-list
> (delq process pinentry--connection-process-list)))
> ;; Delete the associated connection file, if applicable.
> ;; Although there's no 100% guarantee that the file is owned by the
> ;; running Emacs instance, server-start uses server-running-p to check
> ;; for possible servers before doing anything, so it *should* be ours.
> (and (process-contact process :server)
> (eq (process-status process) 'closed)
> (ignore-errors
> (delete-file (process-get process :server-file)))))
>
> (provide 'pinentry)
>
> ;;; pinentry.el ends here
More information about the Gnupg-devel
mailing list