/*	$OpenBSD: ssh-agent.c,v 1.19 1999/11/18 14:00:49 markus Exp $	*/

/*

ssh-agent.c

Author: Tatu Ylonen <ylo@cs.hut.fi>

Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
                   All rights reserved

Created: Wed Mar 29 03:46:59 1995 ylo

The authentication agent program.

*/

#include "includes.h"
RCSID("$OpenBSD: ssh-agent.c,v 1.19 1999/11/18 14:00:49 markus Exp $");

#include "ssh.h"
#include "rsa.h"
#include "authfd.h"
#include "buffer.h"
#include "bufaux.h"
#include "xmalloc.h"
#include "packet.h"
#include "getput.h"
#include "mpaux.h"

#ifdef HAVE_OPENSSL
#include <openssl/md5.h>
#endif
#ifdef HAVE_SSL
#include <ssl/md5.h>
#endif

#ifdef HAVE___PROGNAME
extern char *__progname;
#else /* HAVE___PROGNAME */
const char *__progname = "ssh-agent";
#endif /* HAVE___PROGNAME */

typedef struct
{
  int fd;
  enum { AUTH_UNUSED, AUTH_SOCKET, AUTH_CONNECTION } type;
  Buffer input;
  Buffer output;
} SocketEntry;

unsigned int sockets_alloc = 0;
SocketEntry *sockets = NULL;

typedef struct
{
  RSA *key;
  char *comment;
} Identity;

unsigned int num_identities = 0;
Identity *identities = NULL;

int max_fd = 0;

/* pid of shell == parent of agent */
int parent_pid = -1;

/* pathname and directory for AUTH_SOCKET */
char socket_name[1024];
char socket_dir[1024];

void
process_request_identity(SocketEntry *e)
{
  Buffer msg;
  int i;

  buffer_init(&msg);
  buffer_put_char(&msg, SSH_AGENT_RSA_IDENTITIES_ANSWER);
  buffer_put_int(&msg, num_identities);
  for (i = 0; i < num_identities; i++)
    {
      buffer_put_int(&msg, BN_num_bits(identities[i].key->n));
      buffer_put_bignum(&msg, identities[i].key->e);
      buffer_put_bignum(&msg, identities[i].key->n);
      buffer_put_string(&msg, identities[i].comment, 
			strlen(identities[i].comment));
    }
  buffer_put_int(&e->output, buffer_len(&msg));
  buffer_append(&e->output, buffer_ptr(&msg), buffer_len(&msg));
  buffer_free(&msg);
}

void
process_authentication_challenge(SocketEntry *e)
{
  int i, pub_bits, len;
  BIGNUM *pub_e, *pub_n, *challenge;
  Buffer msg;
  MD5_CTX md;
  unsigned char buf[32], mdbuf[16], session_id[16];
  unsigned int response_type;

  buffer_init(&msg);
  pub_e = BN_new();
  pub_n = BN_new();
  challenge = BN_new();
  pub_bits = buffer_get_int(&e->input);
  buffer_get_bignum(&e->input, pub_e);
  buffer_get_bignum(&e->input, pub_n);
  buffer_get_bignum(&e->input, challenge);
  if (buffer_len(&e->input) == 0)
    {
      /* Compatibility code for old servers. */
      memset(session_id, 0, 16);
      response_type = 0;
    }
  else
    {
      /* New code. */
      buffer_get(&e->input, (char *)session_id, 16);
      response_type = buffer_get_int(&e->input);
    }
  for (i = 0; i < num_identities; i++)
    if (pub_bits == BN_num_bits(identities[i].key->n) &&
	BN_cmp(pub_e, identities[i].key->e) == 0 &&
	BN_cmp(pub_n, identities[i].key->n) == 0)
      {
	/* Decrypt the challenge using the private key. */
	rsa_private_decrypt(challenge, challenge, identities[i].key);

	/* Compute the desired response. */
	switch (response_type)
	  {
	  case 0: /* As of protocol 1.0 */
	    /* This response type is no longer supported. */
	    log("Compatibility with ssh protocol 1.0 no longer supported.");
	    buffer_put_char(&msg, SSH_AGENT_FAILURE);
	    goto send;

	  case 1: /* As of protocol 1.1 */
	    /* The response is MD5 of decrypted challenge plus session id. */
	    len = BN_num_bytes(challenge);

	    if (len <= 0 || len > 32) {
	      fatal("process_authentication_challenge: "
		    "bad challenge length %d", len);
	    }

	    memset(buf, 0, 32);
	    BN_bn2bin(challenge, buf + 32 - len);
	    MD5_Init(&md);
	    MD5_Update(&md, buf, 32);
	    MD5_Update(&md, session_id, 16);
	    MD5_Final(mdbuf, &md);
	    break;

	  default:
	    fatal("process_authentication_challenge: bad response_type %d", 
		  response_type);
	    break;
	  }

	/* Send the response. */
	buffer_put_char(&msg, SSH_AGENT_RSA_RESPONSE);
	for (i = 0; i < 16; i++)
	  buffer_put_char(&msg, mdbuf[i]);

	goto send;
      }
  /* Unknown identity.  Send failure. */
  buffer_put_char(&msg, SSH_AGENT_FAILURE);
 send:
  buffer_put_int(&e->output, buffer_len(&msg));
  buffer_append(&e->output, buffer_ptr(&msg),
		buffer_len(&msg));
  buffer_free(&msg);
  BN_clear_free(pub_e);
  BN_clear_free(pub_n);
  BN_clear_free(challenge);
}

void
process_remove_identity(SocketEntry *e)
{
  unsigned int bits;
  unsigned int i;
  BIGNUM *dummy, *n;
  
  dummy = BN_new();
  n = BN_new();
  
  /* Get the key from the packet. */
  bits = buffer_get_int(&e->input);
  buffer_get_bignum(&e->input, dummy);
  buffer_get_bignum(&e->input, n);

  if (bits != BN_num_bits(n))
    error("Warning: keysize mismatch: actual %d, announced %d",
	  BN_num_bits(n), bits);
  
  /* Check if we have the key. */
  for (i = 0; i < num_identities; i++)
    if (BN_cmp(identities[i].key->n, n) == 0)
      {
	/* We have this key.  Free the old key.  Since we don\'t want to leave
	   empty slots in the middle of the array, we actually free the
	   key there and copy data from the last entry. */
	RSA_free(identities[i].key);
	xfree(identities[i].comment);
	if (i < num_identities - 1)
	  identities[i] = identities[num_identities - 1];
	num_identities--;
	BN_clear_free(dummy);
	BN_clear_free(n);

	/* Send success. */
	buffer_put_int(&e->output, 1);
	buffer_put_char(&e->output, SSH_AGENT_SUCCESS);
	return;
      }
  /* We did not have the key. */
  BN_clear(dummy);
  BN_clear(n);

  /* Send failure. */
  buffer_put_int(&e->output, 1);
  buffer_put_char(&e->output, SSH_AGENT_FAILURE);
}

/* Removes all identities from the agent. */

void
process_remove_all_identities(SocketEntry *e)
{
  unsigned int i;
  
  /* Loop over all identities and clear the keys. */
  for (i = 0; i < num_identities; i++)
    {
      RSA_free(identities[i].key);
      xfree(identities[i].comment);
    }

  /* Mark that there are no identities. */
  num_identities = 0;

  /* Send success. */
  buffer_put_int(&e->output, 1);
  buffer_put_char(&e->output, SSH_AGENT_SUCCESS);
  return;
}

/* Adds an identity to the agent. */

void
process_add_identity(SocketEntry *e)
{
  RSA *k;
  int i;
  BIGNUM *aux;
  BN_CTX *ctx;
  
  if (num_identities == 0)
    identities = xmalloc(sizeof(Identity));
  else
    identities = xrealloc(identities, (num_identities + 1) * sizeof(Identity));

  identities[num_identities].key = RSA_new();
  k = identities[num_identities].key;
  buffer_get_int(&e->input); /* bits */
  k->n = BN_new();
  buffer_get_bignum(&e->input, k->n);
  k->e = BN_new();
  buffer_get_bignum(&e->input, k->e);
  k->d = BN_new();
  buffer_get_bignum(&e->input, k->d);
  k->iqmp = BN_new();
  buffer_get_bignum(&e->input, k->iqmp);
  /* SSH and SSL have p and q swapped */
  k->q = BN_new();
  buffer_get_bignum(&e->input, k->q); /* p */
  k->p = BN_new();
  buffer_get_bignum(&e->input, k->p); /* q */

  /* Generate additional parameters */
  aux = BN_new();
  ctx = BN_CTX_new();

  BN_sub(aux, k->q, BN_value_one());
  k->dmq1 = BN_new();
  BN_mod(k->dmq1, k->d, aux, ctx);

  BN_sub(aux, k->p, BN_value_one());
  k->dmp1 = BN_new();
  BN_mod(k->dmp1, k->d, aux, ctx);

  BN_clear_free(aux);
  BN_CTX_free(ctx);
  
  identities[num_identities].comment = buffer_get_string(&e->input, NULL);

  /* Check if we already have the key. */
  for (i = 0; i < num_identities; i++)
    if (BN_cmp(identities[i].key->n, k->n) == 0)
      {
	/* We already have this key.  Clear and free the new data and
	   return success. */
	RSA_free(k);
	xfree(identities[num_identities].comment);

	/* Send success. */
	buffer_put_int(&e->output, 1);
	buffer_put_char(&e->output, SSH_AGENT_SUCCESS);
	return;
      }

  /* Increment the number of identities. */
  num_identities++;
  
  /* Send a success message. */
  buffer_put_int(&e->output, 1);
  buffer_put_char(&e->output, SSH_AGENT_SUCCESS);
}

void
process_message(SocketEntry *e)
{
  unsigned int msg_len;
  unsigned int type;
  unsigned char *cp;
  if (buffer_len(&e->input) < 5)
    return; /* Incomplete message. */
  cp = (unsigned char *)buffer_ptr(&e->input);
  msg_len = GET_32BIT(cp);
  if (msg_len > 256 * 1024)
    {
      shutdown(e->fd, SHUT_RDWR);
      close(e->fd);
      e->type = AUTH_UNUSED;
      return;
    }
  if (buffer_len(&e->input) < msg_len + 4)
    return;
  buffer_consume(&e->input, 4);
  type = buffer_get_char(&e->input);

  switch (type)
    {
    case SSH_AGENTC_REQUEST_RSA_IDENTITIES:
      process_request_identity(e);
      break;
    case SSH_AGENTC_RSA_CHALLENGE:
      process_authentication_challenge(e);
      break;
    case SSH_AGENTC_ADD_RSA_IDENTITY:
      process_add_identity(e);
      break;
    case SSH_AGENTC_REMOVE_RSA_IDENTITY:
      process_remove_identity(e);
      break;
    case SSH_AGENTC_REMOVE_ALL_RSA_IDENTITIES:
      process_remove_all_identities(e);
      break;
    default:
      /* Unknown message.  Respond with failure. */
      error("Unknown message %d", type);
      buffer_clear(&e->input);
      buffer_put_int(&e->output, 1);
      buffer_put_char(&e->output, SSH_AGENT_FAILURE);
      break;
    }
}

void
new_socket(int type, int fd)
{
  unsigned int i, old_alloc;
  if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0)
    error("fcntl O_NONBLOCK: %s", strerror(errno));

  if (fd > max_fd)
    max_fd = fd;

  for (i = 0; i < sockets_alloc; i++)
    if (sockets[i].type == AUTH_UNUSED)
      {
	sockets[i].fd = fd;
	sockets[i].type = type;
	buffer_init(&sockets[i].input);
	buffer_init(&sockets[i].output);
	return;
      }
  old_alloc = sockets_alloc;
  sockets_alloc += 10;
  if (sockets)
    sockets = xrealloc(sockets, sockets_alloc * sizeof(sockets[0]));
  else
    sockets = xmalloc(sockets_alloc * sizeof(sockets[0]));
  for (i = old_alloc; i < sockets_alloc; i++)
    sockets[i].type = AUTH_UNUSED;
  sockets[old_alloc].type = type;
  sockets[old_alloc].fd = fd;
  buffer_init(&sockets[old_alloc].input);
  buffer_init(&sockets[old_alloc].output);
}

void
prepare_select(fd_set *readset, fd_set *writeset)
{
  unsigned int i;
  for (i = 0; i < sockets_alloc; i++)
    switch (sockets[i].type)
      {
      case AUTH_SOCKET:
      case AUTH_CONNECTION:
	FD_SET(sockets[i].fd, readset);
	if (buffer_len(&sockets[i].output) > 0)
	  FD_SET(sockets[i].fd, writeset);
	break;
      case AUTH_UNUSED:
	break;
      default:
	fatal("Unknown socket type %d", sockets[i].type);
	break;
      }
}

void after_select(fd_set *readset, fd_set *writeset)
{
  unsigned int i;
  int len, sock;
  char buf[1024];
  struct sockaddr_un sunaddr;

  for (i = 0; i < sockets_alloc; i++)
    switch (sockets[i].type)
      {
      case AUTH_UNUSED:
	break;
      case AUTH_SOCKET:
	if (FD_ISSET(sockets[i].fd, readset))
	  {
	    len = sizeof(sunaddr);
	    sock = accept(sockets[i].fd, (struct sockaddr *)&sunaddr, &len);
	    if (sock < 0)
	      {
		perror("accept from AUTH_SOCKET");
		break;
	      }
	    new_socket(AUTH_CONNECTION, sock);
	  }
	break;
      case AUTH_CONNECTION:
	if (buffer_len(&sockets[i].output) > 0 &&
	    FD_ISSET(sockets[i].fd, writeset))
	  {
	    len = write(sockets[i].fd, buffer_ptr(&sockets[i].output),
			buffer_len(&sockets[i].output));
	    if (len <= 0)
	      {
		shutdown(sockets[i].fd, SHUT_RDWR);
		close(sockets[i].fd);
		sockets[i].type = AUTH_UNUSED;
		break;
	      }
	    buffer_consume(&sockets[i].output, len);
	  }
	if (FD_ISSET(sockets[i].fd, readset))
	  {
	    len = read(sockets[i].fd, buf, sizeof(buf));
	    if (len <= 0)
	      {
		shutdown(sockets[i].fd, SHUT_RDWR);
		close(sockets[i].fd);
		sockets[i].type = AUTH_UNUSED;
		break;
	      }
	    buffer_append(&sockets[i].input, buf, len);
	    process_message(&sockets[i]);
	  }
	break;
      default:
	fatal("Unknown type %d", sockets[i].type);
      }
}

void
check_parent_exists(int sig)
{
  if (kill(parent_pid, 0) < 0)
    {
      /* printf("Parent has died - Authentication agent exiting.\n"); */
      exit(1);
    }
  signal(SIGALRM, check_parent_exists);
  alarm(10);
}

void
cleanup_socket(void)
{
  remove(socket_name);
  rmdir(socket_dir);
}

void
cleanup_exit(int i)
{
  cleanup_socket();
  exit(i);
}

void
usage()
{
  fprintf(stderr, "ssh-agent version %s\n", SSH_VERSION);
  fprintf(stderr, "Usage: %s [-c | -s] [-k] [command {args...]]\n",
	  __progname);
  exit(1);
}

int
main(int ac, char **av)
{
  fd_set readset, writeset;
  int sock, c_flag = 0, k_flag = 0, s_flag = 0, ch;
  struct sockaddr_un sunaddr;
  pid_t pid;
  char *shell, *format, *pidstr, pidstrbuf[1 + 3 * sizeof pid];

  /* check if RSA support exists */
  if (rsa_alive() == 0) {
    fprintf(stderr,
      "%s: no RSA support in libssl and libcrypto.  See ssl(8).\n",
      __progname);
    exit(1);
  }

#if defined(__GNU_LIBRARY__)
  while ((ch = getopt(ac, av, "+cks")) != -1)
#else 
  while ((ch = getopt(ac, av, "cks")) != -1)
#endif /* defined(__GNU_LIBRARY__) */
    {
      switch (ch)
	{
	case 'c':
	  if (s_flag)
	    usage();
	  c_flag++;
	  break;
	case 'k':
	  k_flag++;
	  break;
	case 's':
	  if (c_flag)
	    usage();
	  s_flag++;
	  break;
	default:
	  usage();
	}
    }
  ac -= optind;
  av += optind;

  if (ac > 0 && (c_flag || k_flag || s_flag))
    usage();

  if (ac == 0 && !c_flag && !k_flag && !s_flag)
    {
      shell = getenv("SHELL");
      if (shell != NULL && strncmp(shell + strlen(shell) - 3, "csh", 3) == 0)
	c_flag = 1;
    }

  if (k_flag)
    {
      pidstr = getenv(SSH_AGENTPID_ENV_NAME);
      if (pidstr == NULL)
	{
	  fprintf(stderr, "%s not set, cannot kill agent\n",
		  SSH_AGENTPID_ENV_NAME);
	  exit(1);
	}
      pid = atoi(pidstr);
      if (pid < 1)		/* XXX PID_MAX check too */
	{
	  fprintf(stderr, "%s=\"%s\", which is not a good PID\n",
		  SSH_AGENTPID_ENV_NAME, pidstr);
	  exit(1);
	}
      if (kill(pid, SIGTERM) == -1)
	{
	  perror("kill");
	  exit(1);
	}
      format = c_flag ? "unsetenv %s;\n" : "unset %s;\n";
      printf(format, SSH_AUTHSOCKET_ENV_NAME);
      printf(format, SSH_AGENTPID_ENV_NAME);
      printf("echo Agent pid %d killed;\n", pid);
      exit(0);
    }

  parent_pid = getpid();

  /* Create private directory for agent socket */
  strlcpy(socket_dir, "/tmp/ssh-XXXXXXXX", sizeof socket_dir);
  if (mkdtemp(socket_dir) == NULL) {
      perror("mkdtemp: private socket dir");
      exit(1);
  }
  snprintf(socket_name, sizeof socket_name, "%s/agent.%d", socket_dir,
	   parent_pid);

  /* Create socket early so it will exist before command gets run from
     the parent.  */
  sock = socket(AF_UNIX, SOCK_STREAM, 0);
  if (sock < 0)
    {
      perror("socket");
      cleanup_exit(1);
    }
  memset(&sunaddr, 0, sizeof(sunaddr));
  sunaddr.sun_family = AF_UNIX;
  strlcpy(sunaddr.sun_path, socket_name, sizeof(sunaddr.sun_path));
  if (bind(sock, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) < 0)
    {
      perror("bind");
      cleanup_exit(1);
    }
  if (listen(sock, 5) < 0)
    {
      perror("listen");
      cleanup_exit(1);
    }

  /* Fork, and have the parent execute the command, if any, or present the
     socket data.  The child continues as the authentication agent. */
  pid = fork();
  if (pid == -1)
    {
      perror("fork");
      exit(1);
    }
  if (pid != 0)
    { /* Parent - execute the given command. */
      close(sock);
      snprintf(pidstrbuf, sizeof pidstrbuf, "%d", pid);
      if (ac == 0)
	{
	  format = c_flag ? "setenv %s %s;\n" : "%s=%s; export %s;\n";
	  printf(format, SSH_AUTHSOCKET_ENV_NAME, socket_name,
		 SSH_AUTHSOCKET_ENV_NAME);
	  printf(format, SSH_AGENTPID_ENV_NAME, pidstrbuf,
		 SSH_AGENTPID_ENV_NAME);
	  printf("echo Agent pid %d;\n", pid);
	  exit(0);
	}

      setenv(SSH_AUTHSOCKET_ENV_NAME, socket_name, 1);
      setenv(SSH_AGENTPID_ENV_NAME, pidstrbuf, 1);
      execvp(av[0], av);
      perror(av[0]);
      exit(1);
    }

  close(0);
  close(1);
  close(2);

  if (setsid() == -1)
    {
      perror("setsid");
      cleanup_exit(1);
    }

  if (atexit(cleanup_socket) < 0)
    {
      perror("atexit");
      cleanup_exit(1);
    }

  new_socket(AUTH_SOCKET, sock);
  if (ac > 0)
    {
      signal(SIGALRM, check_parent_exists);
      alarm(10);
    }

  signal(SIGINT, SIG_IGN);
  signal(SIGPIPE, SIG_IGN);
  while (1)
    {
      FD_ZERO(&readset);
      FD_ZERO(&writeset);
      prepare_select(&readset, &writeset);
      if (select(max_fd + 1, &readset, &writeset, NULL, NULL) < 0)
	{
	  if (errno == EINTR)
	    continue;
	  exit(1);
	}
      after_select(&readset, &writeset);
    }
  /*NOTREACHED*/
}
