/* krb.c -- This file contains the code to deal with the kerberos
 * exchange.  It knows about this protocol, and will Do The Right 
 * Thing with inter-realm, etc.  Most to the protocol for Charon 
 * is in this file
 *
 * Created by:	Derek Atkins <warlord@MIT.EDU>
 *
 * $Source: /mit/warlord/Thesis/build/src/lib/RCS/krb.c,v $
 * $Author: warlord $
 *
 */

#include <warlord-copyright.h>

#if !defined(lint) && !defined(SABER)
static char rcsid_krb_c[] = "$Id: krb.c,v 1.16 93/12/11 15:15:18 warlord Exp Locker: warlord $";
#endif

#include <stdio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <com_err.h>
#include <krb.h>
#include <des.h>

#include <signal.h>
#include <setjmp.h>

#ifdef POSIX
#include <termios.h>
#else
#include <sgtty.h>
#endif

#ifdef SERVER
#include <sys/time.h>
#include <netdb.h>
#endif /* SERVER */

#include <charon_prot.h>
#include <charon.h>		/* This includes charon_err.h */
#include <parser.h>
#include <rpc.h>

/* Place to put error messages */
static char errbuf[BUFSIZ];

static jmp_buf env;
static void sig_restore();
static void push_signals();
static void pop_signals();

/* This is a handle structure for the state of a file descriptor
 * when we need to turn off echoing.  No one else needs to know about
 * this.
 */
struct _crn_tty {
  int		fd;		/* File Descriptor for this TTY */
#ifdef POSIX
  struct termios tty;
#else
  struct sgttyb tty;
#endif
};

/* Effects:	reverts echo to the state on the handle that is passed
 *	into this function, and then destroys that handle.
 */
void 
crn_revert_echo(void *state)
{
  struct _crn_tty *tty = state;

  if (state == NULL)
    return;

  /* reset state */
#ifndef POSIX
  ioctl(tty->fd, TIOCSETP, &(tty->tty));
#else
#ifdef ultrix
  (void) tcsetattr(tty->fd, TCSANOW, &(tty->tty));
#else
  (void) tcsetattr(tty->fd, TCSAFLUSH, &(tty->tty));
#endif
#endif

  /* destroy the handle */
  free(tty);
}

/* Effects:	turns off echoing on the FD, and returns a handle
 *	to a structure to reset that echoing on that FD.
 */
void *
crn_kill_echo(int fd)
{
#ifdef POSIX
  struct termios crn_ttya;
#else
  struct sgttyb crn_ttya;
#endif
  struct _crn_tty *tty = (struct _crn_tty *)malloc(sizeof(struct _crn_tty));

  /* Allocate a handle, save the state, turn off echo ... */
#ifndef POSIX
  ioctl(fd, TIOCGETP, &crn_ttya);
  ioctl(fd, TIOCGETP, &(tty->tty));
  crn_ttya.sg_flags &= ~ECHO;
  ioctl(fd, TIOCSETP, &crn_ttya);
#else
  (void) tcgetattr(fd, &crn_ttya);
  (void) tcgetattr(fd, &(tty->tty));
  crn_ttya.c_lflag &= ~ECHO;
#ifdef ultrix
  (void) tcsetattr(fd, TCSANOW, &crn_ttya);
#else
  (void) tcsetattr(fd, TCSAFLUSH, &crn_ttya);
#endif
#endif

  /* ... and return the handle */
  tty->fd = fd;
  return((void *)tty);
}

/* This is the key procedure from rkinit.  It might need to be fiddled
 * with to get it to work.  The user can always supply their own 
 * key_proc function, of course.
 *
 * Effects:	Will print out key information, and then get the
 *		user's password, and convert it to a key, and
 * 		return that to the caller.
 */
static int
crn_key_proc(char *user, char *instance, char *realm, char *arg, 
	     des_cblock key)
{
    charon_t *charon = (charon_t *)arg;
    char password[BUFSIZ];
    int ok = 0;
    void *tty_state;

    BCLEAR(password);

    /* 
     * If the username does not match the aname in the ticket, 
     * we will print that too.  Otherwise, we won't.
     */
    
    printf("Charon initialization (%s)", charon->hostinst);
    
    printf("\nPassword for %s%s%s@%s: ", user,
	   (instance[0]) ? "." : "", instance, realm);

    fflush(stdout);

    push_signals();
    if (setjmp(env)) {
	ok = -1;
	goto lose;
    }
    
    tty_state = crn_kill_echo(0);

    if (read(0, password, sizeof(password)) == -1) {
	perror("read");
	ok = -1;
	goto lose;
    }

    if (password[strlen(password)-1] == '\n')
	password[strlen(password)-1] = 0;

     /* Generate the key from the password and destroy the password */

    des_string_to_key(password, key);

lose:
    BCLEAR(password);

    crn_revert_echo(tty_state);

    pop_signals();
    printf("\n");

    return(ok);
}

#ifdef SERVER
/* Effects:	This expects both a TGT and either rcmd or shared
 *		key to be present, and it will then procede to
 *		send them across and start the protocol.  Really!
 */
static int
crn_srv_decrypt_2_tkts(char *user, char *instance, char *realm, char *arg, 
		       int (*key_proc)(), KTEXT *cipp)
{
  int retval;
  charon_t *info = (charon_t *)arg;

  /* XXX to make saber happy */
  user++; instance++; realm++; 

  info->tkt2 = *cipp;

  if ((retval = crn_main_loop(info)) != CRN_OK) {
/*    fprintf(stderr, "Fell out of MainLoop with an error\n"); */
    longjmp(env, retval);
  }
  return(retval);
}

/* Effects:	This expects to have the TGT, and it will then go off
 *		and get the rcmd (or shared key) service ticket.  If
 *		I do get back to here, then I must have successfully
 *		received the TGT.
 */
static int
crn_srv_decrypt_tgt(char *user, char *instance, char *realm, char *arg, 
		    int (*key_proc)(), KTEXT *cipp)
{
  charon_t *info = (charon_t *)arg;
  int same_realm = !strcmp(info->realm, info->hostrealm);

  /* XXX to make saber happy */
  user++; instance++; realm++;

  info->tgt = *cipp;

  /* If we are the same realm, then get rcmd.hostname.  If not,
   * then get krbtgt.realm-of-host.
   */
  krb_get_in_tkt(info->principal, info->instance, info->realm, 
		 (same_realm) ? RCMD : KRBTGT,
		 (same_realm) ? info->hostinst : info->hostrealm,
		 1, NULL, crn_srv_decrypt_2_tkts, arg);

  /* If I've returned from here, then things are good. */
  return(CRN_OK);
}
#endif /* SERVER */

/* This is the server-side main call into the charon library.
 *
 * Requires:	Echo-ing not be a problem (meaning it should
 * 		be turned off), and that the process be owned by
 *		the uid who will own the tickets, and also be able
 *		to read the srvtab file.  This can be done by having
 *		the process's real id be that of the ticket owner, and
 *		the effective uid be root.  In any event, being able to
 *		read the srvtab is more important than owning the tickets.
 *
 * Modifies:	key, enc_ses
 *
 * Effects:	Will get kerberos tickets for principal and instance
 *		in the specified realm, and put them in the specified
 *		file.  If the user doesn't exist, cant store the file,
 *		or this machine doesnt have an rcmd ticket, then
 *		fail with an error to the user.  Key is a pointer
 *		to the user-defined space to hold a session key, and the
 *		session key will be stored there if it is not NULL.
 *		enc_ses is a pointer to a character that will be sent
 *		to the client, and it will contain the binary AND of
 *		the server and client encrypt_session values upon
 *		return.  A NULL pointer is equivalent to no encryption
 *		information, and nothing will be placed here.
 *		The ticket will be stored in the default location.
 *		Returns CRN_OK if everything goes ok, and the ticket is
 *		valid, and all check pass.
 */
int
crn_srv_get_tickets(char *principal, char *instance, char *realm,
		    char *hostrealm, int lifetime, char *ticketfile, 
		    int version, char *enc_ses, des_cblock *key)
{
#ifdef SERVER
  /* All this code is only valid if this library has the server code 
   * built into it.  Make sure, however, that the call is available
   * to the user.
   */
  charon_t info;
  time_t cookie;
  int retval;
  struct hostent *hp;
  char hostname[INST_SZ];
  KTEXT_ST rcmd;

  /* init error table */
  initialize_krb_error_table();

  SBCLEAR(info);
  SBCLEAR(rcmd);

  /* Set it, just in case */
  if (enc_ses != NULL) {
    info.srv_encrypt = *enc_ses;
  } else
    info.srv_encrypt = CRN_ENC_NONE;

  info.cli_encrypt = CRN_ENC_NONE;

  /* Set the mode right here. */
  info.mode = SERVER_MODE;

  /* Copy in the principal, and hostname...  These MUST be
   * provided.  Also set the username to the value provided and
   * copy in the instance, if provided.
   */
  if (!principal || !*principal) {
    fprintf(stderr, "Bad principal\n");
    return(CRN_ERROR);
  }
  if (gethostname(hostname, INST_SZ - 1) != 0) {
    fprintf(stderr, "Cannot get my hostname\n");
    return(CRN_ERROR);
  } else {
    /* Resolve this so we can create a "fake" socket address. */
    if ((hp = gethostbyname(hostname)) == NULL) {
      fprintf(stderr, "Cannot resolve hostname: %s\n", hostname);
      return(CRN_ERROR);
    }

    info.saddr.sin_family = hp->h_addrtype;
    COPY(hp->h_addr, &info.saddr.sin_addr, hp->h_length);

    /* Also get the kerberos hostname for this host. */
    strcpy(info.hostinst, krb_get_phost(hostname));
  }
  strcpy(info.principal, principal);

  if (instance)
    strcpy(info.instance, instance);

  /* Copy in the realm if its here.  Otherwise, default to local realm,
   * no matter how we get it.
   */
  if (realm && *realm)
    strcpy(info.realm, realm);
  else
    if (krb_get_lrealm(info.realm, 1) != KSUCCESS)
      strcpy(info.realm, KRB_REALM);

  /* If the hostrealm isn't provided, then get the host's realm! */
  if (hostrealm && *hostrealm)
    strcpy(info.hostrealm, hostrealm);
  else
    strcpy(info.hostrealm, krb_realmofhost(hostname));

  /* Set the ticket file if we should */
  if (ticketfile && *ticketfile)
    krb_set_tkt_string(ticketfile);

  /* Set the cookie.  This should be set as the current time of this
   * machine.
   */
  cookie = time(0);
  int2octet(cookie, info.cookie);

  /* Find out if we can use the version passed in, otherwise ignore it
   * and choose MAX_VERSION
   */
  if (version < MIN_VERSION || version > MAX_VERSION)
    info.version = MAX_VERSION;
  else
    info.version = version;

  /* Set the session key holder. */
  info.session = key;

  /* Give some space */
  info.rcmd = &rcmd;

  if (geteuid() != 0) {
    fprintf(stderr, "Server not running as root.\n");
    return(CRN_ERROR);
  }

  /* Send this out to stdout... */
  fprintf(stderr, "Charon Initialization... Waiting for Client Connection.\n");

  /* 
   * We need a setjmp here because krb_get_in_tkt ignores the
   * return value of decrypt_tkt.  Thus if we want any of its
   * return values to reach the client, we have to jump out of 
   * the routine.
   */

  if (setjmp(env) == 0) {
    /* Get the tickets */
    if ((retval = krb_get_in_tkt(info.principal, info.instance, info.realm, 
				 KRBTGT, info.realm, lifetime,
				 NULL, crn_srv_decrypt_tgt, (char *)&info))
	!= KSUCCESS) {
      crn_send_error(error_message(retval));
    } else 
      crn_send_success();

  } else {
    crn_send_error(errbuf);
    retval = CRN_ERROR;
  }
  (void)setuid(0);		/* make sure I'm root again, if I was before */

  /* Set this value to the bit-and of the two values */
  if (enc_ses != NULL)
    *enc_ses = (info.srv_encrypt & info.cli_encrypt);

  return(retval);
#else /* !SERVER */

  /* Its not a server.  Always return an error! */
  return(CRN_ERROR);
#endif /* SERVER */
}


/* This is the client-side mail call into the charon library.
 *
 * Requires:	Echo-ing *not* be a problem (or turned off)
 *
 * Modifies:	key, enc_ses
 *
 * Effects:	Will talk to the server and the the TGT data,
 *		decrypt it, and then encrypt the ticket in the
 * 		rcmd of the server and send it back.  Will inform
 *		the user if any errors occur.
 *		key_proc is a pointer to a user-defined procedure, expecting
 *		arguments: (user, instance, realm, arg, key).  If it is
 *		NULL, then use pre-defined function to get
 *		the user's password and create a DES key.
 *		key is a pointer to the user-defined space to hold a
 *		des key, and the session key will be stored there if 
 *		it is not NULL.
 *		enc_ses is a pointer to an encrypt_session value.  It
 *		will be sent across to the server, and upon exit will
 *		contain the binary AND of the server and client bytes.
 *		If it is a NULL pointer, no encryption is assumed.
 *		Returns CRN_OK if everything goes ok.
 */
int
crn_cli_get_tickets(int version, int (*key_proc)(), char *enc_ses,
		    des_cblock *key)
{
  KTEXT_ST tgt, tkt2, rcmd;
  int status, status2;
  charon_t info;

  SBCLEAR(info);
  SBCLEAR(tgt);
  SBCLEAR(tkt2);
  SBCLEAR(rcmd);

  /* init error table */
  initialize_krb_error_table();

  /* This is to prevent losing current tickets */
  krb_set_tkt_string("/dev/null");

  /* Set it, just in case */
  if (enc_ses != NULL) {
    info.cli_encrypt = *enc_ses;
  } else
    info.cli_encrypt = CRN_ENC_NONE;

  info.srv_encrypt = CRN_ENC_NONE;

  /* Set the mode.  No, not the mood, the MODE */
  info.mode = CLIENT_MODE;

  /* Set the key proc */
  if (key_proc != NULL)
    info.key_proc = key_proc;
  else
    info.key_proc = crn_key_proc;

  /* Set the version, if we can. */
  if (version < MIN_VERSION || version > MAX_VERSION)
    info.version = MAX_VERSION;
  else
    info.version = version;

  /* Set the session key space */
  info.session = key;

  /* Put in data for the tickets. */
  info.tgt = &tgt;
  info.tkt2 = &tkt2;
  info.rcmd = &rcmd;

  status = crn_main_loop(&info);
  status2 = crn_get_status();

#ifndef DEBUG
  /* Destroy the local tickets we got -- they're not useful anyways */
  if (info.obtain_ticket)
    dest_tkt();
#endif

  /* Set this return value */
  if (enc_ses != NULL)
    *enc_ses = (info.cli_encrypt & info.srv_encrypt);

  if (status == CRN_OK)
    return(status2);

  return(status);
}

int
crn_cli_decrypt_tickets(charon_t *charon)
{
  KTEXT cip = charon->tgt;
  C_Block key;		/* Key for decrypting cipher */
  Key_schedule key_s;
  
  /* generate a key */
  {
    register int rc;
    rc = (*charon->key_proc)(charon->principal, charon->instance, 
			     charon->realm, (char *)charon, key);
    if (rc)
      return(rc);
  }
  
  des_key_sched(key, key_s);
  
  /* Decrypt information from KDC */
  des_pcbc_encrypt((C_Block *)cip->dat,(C_Block *)cip->dat,
		   (long) cip->length, key_s, key, 0);
  
  /* Decrypt the second ticket */
  cip = charon->tkt2;
  des_pcbc_encrypt((C_Block *)cip->dat,(C_Block *)cip->dat,
		   (long) cip->length, key_s, key, 0);
  
  /* Get rid of all traces of key */
  BCLEAR(key);
  BCLEAR(key_s);
  
  return(CRN_OK);
}

/* Effects:	Takes the character c and will encrypt it in the
 *		session key supplied and return that value.  It will
 *		Take the value of mode and enc_ses to figure out
 *		how to encrypt this character.
 */
char
crn_encrypt_char(char c, des_cblock session, int mode, char enc_ses)
{
  if ((mode == CLIENT_MODE && enc_ses == CRN_ENC_CLI) || 
      (mode == SERVER_MODE && enc_ses == CRN_ENC_SRV) || 
      enc_ses == CRN_ENC_BOTH)
    return(c + 1);
  else
    return(c);
}

/* Effects:	Takes the character c and will decrypt it using the
 *		session key provided, and return that value.
 */
char
crn_decrypt_char(char c, des_cblock session, int mode, char enc_ses)
{
  if ((mode == CLIENT_MODE && enc_ses == CRN_ENC_SRV) ||
      (mode == SERVER_MODE && enc_ses == CRN_ENC_CLI) ||
      enc_ses == CRN_ENC_BOTH)
    return(c - 1);
  else
    return(c);
}

/* Effects:	sets the errbuf to msg */
int
crn_set_errbuf(char *msg)
{
  strncpy(errbuf, msg, sizeof(errbuf));
}

#ifdef POSIX
static void (*old_sigfunc[NSIG])();
#else
static int (*old_sigfunc[NSIG])();
#endif POSIX

static void push_signals()
{
    register i;
    for (i = 0; i < NSIG; i++)
        old_sigfunc[i] = signal(i,sig_restore);
}

static void pop_signals()
{
    register i;
    for (i = 0; i < NSIG; i++)
        signal(i,old_sigfunc[i]);
}

static void sig_restore(sig,code,scp)
    int sig,code;
    struct sigcontext *scp;
{
    longjmp(env,1);
}
