/* crn.c -- This file contains the code to do the state machine for
 * the Charon protocol.  All the state code should be done in this
 * file, and it should call other functions depending on the state,
 * incoming packet type, and application type.  I was considering making
 * it fairly table-driven, as best as possible.
 *
 * Created by:	Derek Atkins <warlord@MIT.EDU>
 *
 * $Source: /afs/net.mit.edu/user/warlord/Thesis/src/lib/RCS/crn.c,v $
 * $Author: warlord $
 *
 */

#include <warlord-copyright.h>

#if !defined(lint) && !defined(SABER)
static char rcsid_crn_c[] = "$Id: crn.c,v 1.9 93/12/18 23:59:41 warlord Exp Locker: warlord $";
#endif

#include <com_err.h>
#include <krb_err.h>
#include <charon_prot.h>
#include <charon_err.h>
#include <rpc.h>
#include <parser.h>
#include "table.h"

static char errbuf[BUFSIZ];

/* This loop is the main dispatch loop of the Charon system.  It
 * called by both the client and server side, and should most likely
 * be table driven.
 *
 * Effects:	Uses the mode (CLIENT_MODE or SERVER_MODE) to know what
 *		to expect from the other side, and what it should do.
 *		Uses the version passed in to know which protocol to
 *		try to use.  The first thing ALL protocols do is
 *		exchange version info, and they will chose the minimum
 *		of the two possible versions.
 */
int
crn_main_loop(charon_t *charon)
{
  int state = 0;		/* state of the machine */
  int retval;			/* return values from functions */
  int state_changed = 1;	/* did we change state this loop? */
  int maxstate;			/* highest state in table */
  u_char pkt;
  int pkt_type;
  u_char *data;
  u_int datalen;
  v_table version_table = command_table[charon->mode];
  d_table dispatch_table;

  /* Lets go into the table.  We will keep going while retval == 0
   * and we have a state to go to.
   */

  if (version_table == NULL)
    return (CRN_BAD_VERSION);

  do {
    /* The dispatch table can change if the version number
     * changes.  It should ONLY change when processing the
     * version number
     */
    if (charon->version > MAX_VERSION || charon->version < MIN_VERSION) {
      retval = CRN_BAD_VERSION;
      break;
    }

    dispatch_table = version_table[charon->version];

    /* Make sure that this is a valid version table! */
    if (dispatch_table == NULL) {
      retval = CRN_BAD_VERSION;
      break;
    }

    /* Just the maxstate for this table. */
    maxstate = dispatch_table[0].maxstate;

    /* See if we changed state and if we have a function to enter
     * the state.  If both are true, then call that function.
     */
    if (dispatch_table[state].entry != NULL && state_changed)
      (dispatch_table[state].entry)(charon);

    /* Wait for input */
    retval = crn_wait_for_data(&data, &datalen, &pkt);
    pkt_type = pkt;
    if (retval != CRN_OK)
      break;

    charon->data = data;
    charon->datalen = datalen;

    /* Make sure we have a function for this packet type, and if
     * so, then call it!  If not, then set retval = Bad Packet and break
     * from this loop.
     */
    if (dispatch_table[state].fcn[pkt_type] != NULL) 
      retval = (dispatch_table[state].fcn[pkt_type])(charon);
    else {
      retval = CRN_UNKNOWN_PKT_TYPE;
      break;
    }

    /* Free this up, if there was anything. */
    if (charon->datalen)
      free(charon->data);

    /* Get the new state, if we were ok */
    if (retval == CRN_OK) {
      retval = dispatch_table[state].next_state[pkt_type];
      state_changed = retval - state;
      state = retval;
      retval = CRN_OK;
    }

  } while (retval == CRN_OK && state >=0 && state <= maxstate);

  return(retval);
}

static int
CheckStatus(charon_t *charon)
{
  return(crn_read_status((char *)charon->data, charon->datalen));
}

/* Table Procedure Call to send version to other side. */
static int
SendVersionInfo(charon_t *charon)
{
  return(SendVersion(charon->mode, charon->version));
}

/* Effects:	Sends the version across to the other side
 */
static int
SendVersion(int mode, int version)
{
  u_char data[4];
  int retval;

  int2octet(version, data);

  if (mode == CLIENT_MODE)
    retval = crn_send_data(data, 4, PKT_CVERSION);
  else
    retval = crn_send_data(data, 4, PKT_SVERSION);

  return(retval);
}

/* Modifies:	charon
 * Effects:	sets version to the minimum of version, and the
 *		version of the other side, which I have in "data".
 *		If its SERVER_MODE, then send my version to the other
 *		side, since 
 *
 *		A server-side call means that we need to send the version
 *		across to the other side.
 *
 *		returns an error if the minimum is less that MIN_VERSION.
 *		returns CRN_OK on success.
 */
static int
ProcessVersion(charon_t *charon)
{
  int new_version;
  int other_version;

  octet2int(charon->data, &other_version);

  /* The client would have sent it already.  But the server needs
   * to send this information.
   */
  if (charon->mode == SERVER_MODE)
    SendVersion(SERVER_MODE, charon->version);

  new_version = min(charon->version, other_version);

  if (new_version < MIN_VERSION)
    return(CRN_BAD_VERSION);

  charon->version = new_version;
  return(CRN_OK);
}

/* Modifies:	charon
 * Effects:	this will take the data input, and unparse it into
 * 		the charon structure, using the unparse routine.
 *		Different versions may have different structures, so
 *		if the structure changes, it may be necessary.
 *
 *		This should only be called on a client side.
 *
 *		Returns CRN_OK on success, or an error.
 */
static int
ProcessInfo(charon_t *charon)
{
  return(octet2info(charon->data, charon));
}

/* This procedure sends two packets across, the information
 * packet, and then the two KDC packets.  The two KDC packets
 * are the TGT, and either the rcmd or shared key service tickets
 *
 * This should only be called from a Server side!
 *
 * Requires:	the charon structure be complete, and have the
 *		TGT and either rcmd or shared service key in place
 *		in the KTEXT spots.
 * Effects:	Will send the info, tgt, and tkt2 packets across
 *		to the other side.  It will return CRN_OK on success,
 *		or whatever error gets returned from a sub-procedure.
 */
static int
SendInfoAndKDCPkts(charon_t *charon)
{
  int retval;

  /* First, convert my charon structure to something I can send
   * across, and send the info string.
   */

  if ((retval = info2octet(charon, &(charon->data), &(charon->datalen))) 
      != CRN_OK) {
    free(charon->data);
    return(retval);
  }

  if ((retval = crn_send_data(charon->data, charon->datalen, PKT_INFO))
      != CRN_OK) {
    free(charon->data);
    return(retval);
  }

  free(charon->data);

  /* Ok, now we get to send the two KTEXT structures. */
  if ((retval = crn_rpc_put_ktext(charon->tgt, charon->mode)) != CRN_OK)
    return(retval);

  if ((retval = crn_rpc_put_ktext(charon->tkt2, charon->mode)) != CRN_OK)
    return(retval);

  return(CRN_OK);  
}

/* Effects:	Takes the incoming data, which should be the TGT,
 *		converts it to a KTEXT, then gets another packet,
 *		which should be another KTEXT packet, which is
 *		the second ticket, either rcmd.hostinst or the
 *		shared key.  It then will decrypt these with the
 *		user's password, and then, if need be, request the
 *		rcmd ticket.  This will then send the auth data
 *		back to the server.  The auth data is the rcmd ticket,
 *		the encrypted cookie, and the encrypted TGT, in that order.
 *
 *		This should only called on the client side.
 *
 *		Returns CRN_OK on success, or an error.
 */
static int
ProcessServerKDC(charon_t *charon)
{
  int retval;

  if ((retval = DoProcessServerKDC(charon)) != CRN_OK) {
    char errmsg[BUFSIZ];

    sprintf(errmsg, "Count not process server kerberos data, errno=%d",
	    retval);

    crn_send_error(errmsg);
  }

  return(retval);
}

static int
DoProcessServerKDC(charon_t *charon)
{
  int retval;
  KTEXT_ST auth;
  CREDENTIALS cred;
  des_cblock key;
  des_key_schedule sched;
  MSG_DAT tgt_data;
  MSG_DAT cookie_data;
  u_char cookie[5];
  u_char tgt_enc_data[MAX_KTXT_LEN];
  u_char cookie_enc_data[MAX_KTXT_LEN];

  /* First, convert the first packet to ktext */
  if ((retval = octet2ktext(charon->data, charon->datalen, charon->tgt))
      != CRN_OK)
    return(retval);

  /* Now, get the second ktext packet */
  if ((retval = crn_rpc_get_ktext(charon->tkt2))
      != CRN_OK)
    return(retval);

  /* Try to decrypt the tickets */
  if ((retval = crn_cli_decrypt_tickets(charon)) != CRN_OK)
    return(retval);

  /* Now see if we need to get an rcmd ticket.  This can be done
   * by checking if the princial realm and host realm are the same.
   * if they are, then we can send the authentication data, otherwise
   * we will have to get the rcmd ticket.
   */

  /* Store tkt2 into a file for reference */
  if ((retval = crn_store_ticket(charon, 0, 1)) != CRN_OK)
    return(retval);

  if (strcmp(charon->realm, charon->hostrealm)) 
    if ((retval = crn_get_rcmd_tkt(charon)) != CRN_OK)
      return(retval);
  
  /* If we've gotten here, then we have valid tickets for a TGT and
   * an rcmd ticket.  So, we need to encrypt everything and ship it
   * across!
   */
  
  if ((retval = krb_mk_req(&auth, RCMD, charon->hostinst, charon->hostrealm, 0)) 
      != KSUCCESS) 
    return(retval);
  
  /* Re-encrypt server KDC packet in session key 
   * Get credentials from ticket file 
   */
  if ((retval = krb_get_cred(RCMD, charon->hostinst, charon->hostrealm, &cred)) 
      != KSUCCESS) 
    return(retval);
  
  /* Exctract the session key and make the schedule */
  COPY(cred.session, key, sizeof(key));
  if ((retval = des_key_sched(key, sched)) != KSUCCESS) 
    return(retval);
  
  /* Save the session key, if we should */
  if (charon->session != NULL)
    COPY(key, (charon->session), sizeof(des_cblock));
  
  /* Encrypt the TGT in this session key */
  tgt_data.app_data = tgt_enc_data;
  if ((tgt_data.app_length = 
       krb_mk_priv(charon->tgt->dat, tgt_data.app_data, charon->tgt->length, 
		   sched, key, &charon->saddr, &charon->saddr)) == -1) {
    return(CRN_MK_PRIV);
  }

  /* Encrypt the cookie in this session key */
  cookie_data.app_data = cookie_enc_data;
  COPY(charon->cookie, cookie, 4);
  cookie[4] = charon->cli_encrypt;
  if ((cookie_data.app_length = 
       krb_mk_priv(cookie, cookie_data.app_data, 5,
		   sched, key, &charon->saddr, &charon->saddr)) == -1) {
    return(CRN_MK_PRIV);
  }

  if ((retval = crn_rpc_sendauth(&auth)) != CRN_OK)
    return(retval);

  if ((retval = crn_rpc_senddat(&tgt_data)) != CRN_OK)
    return(retval);

  if ((retval = crn_rpc_senddat(&cookie_data)) != CRN_OK)
    return(retval);

  return(CRN_OK);
}

/* Effects:	This is going to be a request for the rcmd ticket.
 *		Just send it off the the KDC, then take the returned
 *		packet, and send that back to the other side.
 *
 *		This should only be called on the server side.
 *
 *		Returns CRN_OK on success, or an error.
 */
static int
ProcessClientKDC(charon_t *charon)
{
  int retval;

  /* First, convert the first packet to ktext */
  if ((retval = octet2ktext(charon->data, charon->datalen, charon->tkt2))
      != CRN_OK)
    return(retval);

  /* Now send this off to the KDC */
  if ((retval = send_to_kdc(charon->tkt2, charon->rcmd, charon->hostrealm))
      != KSUCCESS) {
    sprintf(errbuf, "%s while trying to send_to_kdc", 
	    error_message(ERROR_TABLE_BASE_krb+retval));
    crn_send_error(errbuf);
    return(retval);
  }
  crn_send_success();

  /* XXX -- If there is zero length on this data, then set it to
   * MAX_KTXT_LEN, since the kerberos library may be dain-bramaged
   * and not set the length.
   */
  if (!charon->rcmd->length)
    charon->rcmd->length = MAX_KTXT_LEN;

  /* Ok, return this off to the other side. */
  return(crn_rpc_put_ktext(charon->rcmd, charon->mode));  
}

/* Effects:	Expects three packets:  a ticket, an encrypted cookie,
 *		and an encrypted tgt.  It will verify that the cookie
 *		is the same, that the tgt is for the user in question
 *		(match the username?).  If this is the case, then save
 *		the tgt off to a file, and return CRN_OK.  Return an
 *		error, otherwise.
 *
 *		This is only called on the server side.
 */
static int
ProcessCliAuth(charon_t *charon)
{
  KTEXT_ST auth;
  AUTH_DAT auth_dat;
  MSG_DAT msg_data;
  MSG_DAT tgt_data;
  MSG_DAT cookie_data;
  u_char tgt_enc_data[MAX_KTXT_LEN];
  u_char cookie_enc_data[MAX_KTXT_LEN];
  des_cblock key;
  des_key_schedule sched;
  int status;

  /* Clear all these structures */
  SBCLEAR(auth);
  SBCLEAR(auth_dat);
  SBCLEAR(tgt_data);
  SBCLEAR(cookie_data);
  BCLEAR(tgt_enc_data);
  BCLEAR(cookie_enc_data);

  tgt_data.app_data = tgt_enc_data;
  cookie_data.app_data = cookie_enc_data;

  /* These are sent in a group */
  if ((status = octet2ktext(charon->data, charon->datalen, &auth)) != CRN_OK)
    return(status);
  if ((status = crn_rpc_getdat(&tgt_data)) != CRN_OK)
    return(status);
  if ((status = crn_rpc_getdat(&cookie_data)) != CRN_OK)
    return(status);

  /* Read in the request */
  if ((status = krb_rd_req(&auth, RCMD, charon->hostinst, 
			   0L, &auth_dat, KEYFILE)) != KSUCCESS) {
    if (status != RD_AP_TIME) {
      /* Ignore time-skew problems */
      sprintf(errbuf, "%s in krb_rd_req", 
	      error_message(ERROR_TABLE_BASE_krb+status));
      crn_set_errbuf(errbuf);
      return(status);
    }
  }

  /* copy the session key, and create the key schedule */
  COPY(auth_dat.session, key, sizeof(key));
  if ((status = des_key_sched(key, sched)) != 0) {
    sprintf(errbuf, "%s in des_key_sched", 
	    error_message(ERROR_TABLE_BASE_krb+status));
    crn_set_errbuf(errbuf);
    return(status);
  }
  
  /* Copy off the session key, if we should */
  if (charon->session != NULL)
    COPY(key, (charon->session), sizeof(des_cblock));
  
  /* Decrypt the data and store the TGT. */
  if ((status = 
       krb_rd_priv((u_char *)tgt_data.app_data, tgt_data.app_length, 
		   sched, key, &charon->saddr, &charon->saddr, &msg_data)) 
      == KSUCCESS || status == RD_AP_TIME) {
    /* Ignore Time Skew */
    charon->tgt->length = msg_data.app_length;
    COPY(msg_data.app_data, charon->tgt->dat, msg_data.app_length);
    charon->tgt->dat[charon->tgt->length] = 0;
  } else {
    sprintf(errbuf, "%s in krb_rd_priv of TGT", 
	    error_message(ERROR_TABLE_BASE_krb+status));
    crn_set_errbuf(errbuf);
    return(status);
  }

  SBCLEAR(msg_data);
  /* Decrypt the cookie and store the charon structure. */
  if ((status = 
       krb_rd_priv((u_char *)cookie_data.app_data, cookie_data.app_length, 
		   sched, key, &charon->saddr, &charon->saddr, &msg_data)) 
      == KSUCCESS || status == RD_AP_TIME) {
    /* Again, I don't care about time-skew */
    if (msg_data.app_length != 5) {
      crn_set_errbuf("Bad Cookie Length.");
      return(CRN_ERR_COOKIE);
    }

    /* Compare the cookie to make sure they are the same! */
    if (CMP(msg_data.app_data, charon->cookie, 4)) {
      crn_set_errbuf("Cookie Mismatch.");
      return(CRN_ERR_COOKIE);
    }
    /* Set the value of the encryption flag */
    charon->cli_encrypt = msg_data.app_data[4];

  } else {
    sprintf(errbuf, "%s in krb_rd_priv of cookie", 
	    error_message(ERROR_TABLE_BASE_krb+status));
    crn_set_errbuf(errbuf);
    return(status);
  }

  return(CRN_OK);
}

