You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

706 lines
16 KiB

/* 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 <https://www.gnu.org/licenses/>.
* SPDX-License-Identifier: GPL-2.0+
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif
#ifdef HAVE_INTTYPES_H
#include <inttypes.h>
#endif
#include <assert.h>
#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 <assuan.h>
#include "pinentry-emacs.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$(id -u)/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
static int initial_timeout = INITIAL_TIMEOUT;
#undef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#undef MAX
#define MAX(x, y) ((x) < (y) ? (y) : (x))
#ifndef SUN_LEN
# define SUN_LEN(ptr) ((size_t) (((struct sockaddr_un *) 0)->sun_path) \
+ strlen ((ptr)->sun_path))
#endif
/* FIXME: We could use the I/O functions in Assuan directly, once
Pinentry links to libassuan. */
static int emacs_socket = -1;
static char send_buffer[SEND_BUFFER_SIZE + 1];
static int send_buffer_length; /* Fill pointer for the send buffer. */
static pinentry_cmd_handler_t fallback_cmd_handler;
#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;
char *socket_name_storage = NULL;
uid_t uid;
unaddr.sun_family = AF_UNIX;
/* We assume 32-bit UIDs, which can be represented with 10 decimal
digits. */
uid = getuid ();
if (uid != (uint32_t) uid)
{
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";
}
socket_name_storage = malloc (strlen (tmpdir)
+ strlen ("/emacs") + 10 + strlen ("/")
+ strlen (socket_name)
+ 1);
if (!socket_name_storage)
{
fprintf (stderr, "out of core\n");
free (tmpdir_storage);
return 0;
}
sprintf (socket_name_storage, "%s/emacs%u/%s", tmpdir,
(uint32_t) uid, socket_name);
free (tmpdir_storage);
if (strlen (socket_name_storage) >= sizeof (unaddr.sun_path))
{
fprintf (stderr, "socket name is too long\n");
free (socket_name_storage);
return 0;
}
strcpy (unaddr.sun_path, socket_name_storage);
free (socket_name_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);
emacs_socket = -1;
return 0;
}
return 1;
}
/* Percent-escape control characters in DATA. Return a newly
allocated string. */
static char *
escape (const char *data)
{
char *buffer, *out_p;
size_t length, buffer_length;
size_t offset;
size_t count = 0;
length = strlen (data);
for (offset = 0; offset < length; offset++)
{
switch (data[offset])
{
case '%': case '\n': case '\r':
count++;
break;
default:
break;
}
}
buffer_length = length + count * 2;
buffer = malloc (buffer_length + 1);
if (!buffer)
return NULL;
out_p = buffer;
for (offset = 0; offset < length; offset++)
{
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;
}
}
*out_p = '\0';
return buffer;
}
/* The inverse of escape. Unlike escape, it removes quoting in string
DATA by modifying the string in place, to avoid copying of secret
data sent from Emacs. */
static char *
unescape (char *data)
{
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;
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;
length = strlen (buffer);
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));
send_buffer_length = 0;
return 0;
}
if (sent != send_buffer_length)
memmove (send_buffer, &send_buffer[sent],
send_buffer_length - sent);
send_buffer_length -= sent;
}
length -= part;
}
return 1;
}
/* Read a server response. If the response contains data, it will be
stored in BUFFER with a terminating NUL byte. BUFFER must be
at least as large as CAPACITY. */
static gpg_error_t
read_from_emacs (int s, int timeout, char *buffer, size_t capacity)
{
struct timeval tv;
fd_set rfds;
int retval;
/* Offset in BUFFER. */
size_t offset = 0;
int got_response = 0;
char read_buffer[LINELENGTH + 1];
/* Offset in READ_BUFFER. */
size_t read_offset = 0;
gpg_error_t result = 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 gpg_error (GPG_ERR_ASS_GENERAL);
}
else if (retval == 0)
{
timed_out = 1;
return gpg_error (GPG_ERR_TIMEOUT);
}
/* Loop until we get either OK or ERR. */
while (!got_response)
{
int rl = 0;
char *p, *end_p;
do
{
errno = 0;
rl = recv (s, read_buffer + read_offset, LINELENGTH - 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");
return gpg_error (GPG_ERR_ASS_GENERAL);;
}
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 away the content
and start over the buffering.
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;
continue;
}
/* 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;
size_t needed_capacity;
data = p + 2;
data_length = end_p - data;
if (data_length > 0)
{
needed_capacity = offset + data_length + 1;
/* Check overflow. This is unrealistic but can
happen since OFFSET is cumulative. */
if (needed_capacity < offset)
return gpg_error (GPG_ERR_ASS_GENERAL);;
if (needed_capacity > capacity)
return gpg_error (GPG_ERR_ASS_GENERAL);;
memcpy (&buffer[offset], data, data_length);
offset += data_length;
buffer[offset] = 0;
}
}
else if (!strcmp ("OK", p) || !strncmp ("OK ", p, 3))
{
got_response = 1;
break;
}
else if (!strncmp ("ERR ", p, 4))
{
unsigned long code = strtoul (p + 4, NULL, 10);
if (code == ULONG_MAX && errno == ERANGE)
return gpg_error (GPG_ERR_ASS_GENERAL);
else
result = code;
got_response = 1;
break;
}
else if (*p == '#')
;
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;
}
}
return result;
}
int
set_label (pinentry_t pe, const char *name, const char *value)
{
char buffer[16], *escaped;
gpg_error_t error;
int retval;
if (!send_to_emacs (emacs_socket, name)
|| !send_to_emacs (emacs_socket, " "))
return 0;
escaped = escape (value);
if (!escaped)
return 0;
retval = send_to_emacs (emacs_socket, escaped)
&& send_to_emacs (emacs_socket, "\n");
free (escaped);
if (!retval)
return 0;
error = read_from_emacs (emacs_socket, pe->timeout, buffer, sizeof (buffer));
return error == 0;
}
static void
set_labels (pinentry_t pe)
{
char *p;
p = pinentry_get_title (pe);
if (p)
{
set_label (pe, "SETTITLE", p);
free (p);
}
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 *buffer, *password;
size_t length = LINELENGTH;
gpg_error_t error;
set_labels (pe);
if (!send_to_emacs (emacs_socket, "GETPIN\n"))
return -1;
buffer = secmem_malloc (length);
if (!buffer)
{
pe->specific_err = gpg_error (GPG_ERR_ENOMEM);
return -1;
}
error = read_from_emacs (emacs_socket, pe->timeout, buffer, length);
if (error != 0)
{
if (gpg_err_code (error) == GPG_ERR_CANCELED)
pe->canceled = 1;
secmem_free (buffer);
pe->specific_err = error;
return -1;
}
password = unescape (buffer);
pinentry_setbufferlen (pe, strlen (password) + 1);
if (pe->pin)
strcpy (pe->pin, password);
secmem_free (buffer);
if (pe->repeat_passphrase)
pe->repeat_okay = 1;
/* XXX: we don't support external password cache (yet). */
return 1;
}
static int
do_confirm (pinentry_t pe)
{
char buffer[16];
gpg_error_t error;
set_labels (pe);
if (!send_to_emacs (emacs_socket, "CONFIRM\n"))
return 0;
error = read_from_emacs (emacs_socket, pe->timeout, buffer, sizeof (buffer));
if (error != 0)
{
if (gpg_err_code (error) == GPG_ERR_CANCELED)
pe->canceled = 1;
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;
}
static int
initial_emacs_cmd_handler (pinentry_t pe)
{
/* Let the select() call in pinentry_emacs_init honor the timeout
value set through an Assuan option. */
initial_timeout = pe->timeout;
if (emacs_socket < 0)
pinentry_emacs_init ();
/* If we have successfully connected to Emacs, swap
pinentry_cmd_handler to emacs_cmd_handler, so further
interactions will be forwarded to Emacs. Otherwise, set it back
to the original command handler saved as
fallback_cmd_handler. */
if (emacs_socket < 0)
pinentry_cmd_handler = fallback_cmd_handler;
else
{
pinentry_cmd_handler = emacs_cmd_handler;
pinentry_set_flavor_flag ("emacs");
}
return (* pinentry_cmd_handler) (pe);
}
void
pinentry_enable_emacs_cmd_handler (void)
{
const char *envvar;
/* Check if pinentry_cmd_handler is already prepared for Emacs. */
if (pinentry_cmd_handler == initial_emacs_cmd_handler
|| pinentry_cmd_handler == emacs_cmd_handler)
return;
/* Check if INSIDE_EMACS envvar is set. */
envvar = getenv ("INSIDE_EMACS");
if (!envvar || !*envvar)
return;
/* Save the original command handler as fallback_cmd_handler, and
swap pinentry_cmd_handler to initial_emacs_cmd_handler. */
fallback_cmd_handler = pinentry_cmd_handler;
pinentry_cmd_handler = initial_emacs_cmd_handler;
}
int
pinentry_emacs_init (void)
{
char buffer[256];
gpg_error_t error;
assert (emacs_socket < 0);
/* Check if we can connect to the Emacs server socket. */
if (!set_socket ("pinentry"))
return 0;
/* Check if the server responds. */
error = read_from_emacs (emacs_socket, initial_timeout,
buffer, sizeof (buffer));
if (error != 0)
{
close (emacs_socket);
emacs_socket = -1;
return 0;
}
return 1;
}