/* 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 "../secmem/secmem.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;
}
