diff options
Diffstat (limited to 'debian/pinentry-tqt/pinentry-tqt-1.2.1/pinentry/pinentry-emacs.c')
-rw-r--r-- | debian/pinentry-tqt/pinentry-tqt-1.2.1/pinentry/pinentry-emacs.c | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/debian/pinentry-tqt/pinentry-tqt-1.2.1/pinentry/pinentry-emacs.c b/debian/pinentry-tqt/pinentry-tqt-1.2.1/pinentry/pinentry-emacs.c new file mode 100644 index 00000000..9685b67d --- /dev/null +++ b/debian/pinentry-tqt/pinentry-tqt-1.2.1/pinentry/pinentry-emacs.c @@ -0,0 +1,721 @@ +/* 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_cancel) + 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; +} + + +/* Returns true if the Emacs pinentry is enabled. The value is 1 + * before the first connection with Emacs has been done and 2 if the + * connection to Emacs has been establish. Returns false if the Emacs + * pinentry is not enabled. */ +int +pinentry_emacs_status (void) +{ + if (pinentry_cmd_handler == initial_emacs_cmd_handler) + return 1; + else if (pinentry_cmd_handler == emacs_cmd_handler) + return 2; + else + return 0; +} + +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; +} |