/*
 * chap.c - Crytographic Handshake Authentication Protocol.
 *
 * Copyright (c) 1991 Gregory M. Christy.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms are permitted
 * provided that the above copyright notice and this paragraph are
 * duplicated in all such forms and that any documentation,
 * advertising materials, and other materials related to such
 * distribution and use acknowledge that the software was developed
 * by Gregory M. Christy.  The name of the author may not be used to
 * endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

/*
 * TODO:
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <syslog.h>
#include <sys/socket.h>
#include <net/if.h>
#include <sys/stream.h>
#include "ppp.h"
#include "fsm.h"
#include "lcp.h"
#include "chap.h"
#include "upap.h"
#include "ipcp.h"
#include "md5.h"

chap_state chap[NPPP];		/* CHAP state; one for each unit */

static void ChapTimeout __ARGS((caddr_t));
static void ChapReceiveChallenge __ARGS((chap_state *, u_char *, u_char, int));
static void ChapReceiveResponse __ARGS((chap_state *, u_char *, u_char, int));
static void ChapReceiveSuccess __ARGS((chap_state *, u_char *, u_char, int));
static void ChapReceiveFailure __ARGS((chap_state *, u_char *, u_char, int));
static void ChapSendStatus __ARGS((chap_state *, u_char, u_char,
				   u_char *, int));
static void ChapSendChallenge __ARGS((chap_state *));
static void ChapSendResponse __ARGS((chap_state *, u_char, u_char *, int));
static void ChapGenChallenge __ARGS((u_char, u_char *));

extern double drand48 __ARGS((void));

/*
 * ChapInit - Initialize a CHAP unit.
 */
void
  ChapInit(unit)
int unit;
{
  chap_state *cstate = &chap[unit];

  cstate->unit = unit;
  cstate->chal_str[0] = '\000';
  cstate->chal_len = 0;
  cstate->clientstate = CHAPCS_CLOSED;
  cstate->serverstate = CHAPSS_CLOSED;
  cstate->flags = 0;
  cstate->id = 0;
  cstate->timeouttime = CHAP_DEFTIMEOUT;
  cstate->retransmits = 0;
  srand48((long) time(NULL));	/* joggle random number generator */
}


/*
 * ChapAuthWithPeer - Authenticate us with our peer (start client).
 *
 */
void
  ChapAuthWithPeer(unit)
int unit;
{
  chap_state *cstate = &chap[unit];
  
  cstate->flags &= ~CHAPF_AWPPENDING;	/* Clear pending flag */
  
  /* Protect against programming errors that compromise security */
  if (cstate->serverstate != CHAPSS_CLOSED ||
      cstate->flags & CHAPF_APPENDING) {
    CHAPDEBUG((LOG_INFO,
	       "ChapAuthWithPeer: we were called already!"))
    return;
  }
  
  if (cstate->clientstate == CHAPCS_CHALLENGE_SENT ||  /* should we be here? */
      cstate->clientstate == CHAPCS_OPEN)
    return;
  
  /* Lower layer up? */
  if (!(cstate->flags & CHAPF_LOWERUP)) {
    cstate->flags |= CHAPF_AWPPENDING; /* Nah, Wait */
    return;
  }
  ChapSendChallenge(cstate);		/* crank it up dude! */
  TIMEOUT(ChapTimeout, (caddr_t) cstate, cstate->timeouttime); 
					/* set-up timeout */
  cstate->clientstate = CHAPCS_CHALLENGE_SENT; /* update state */
  cstate->retransmits = 0;
}


/*
 * ChapAuthPeer - Authenticate our peer (start server).
 */
void
  ChapAuthPeer(unit)
int unit;
{
  chap_state *cstate = &chap[unit];
  
  cstate->flags &= ~CHAPF_APPENDING;	/* Clear pending flag */
  
  /* Already authenticat{ed,ing}? */
  if (cstate->serverstate == CHAPSS_LISTEN ||
      cstate->serverstate == CHAPSS_OPEN)
    return;
  
  /* Lower layer up? */
  if (!(cstate->flags & CHAPF_LOWERUP)) {
    cstate->flags |= CHAPF_APPENDING;	/* Wait for desired event */
    return;
  }
  cstate->serverstate = CHAPSS_LISTEN;
}


/*
 * ChapTimeout - Timeout expired.
 */
static void
  ChapTimeout(arg)
caddr_t arg;
{
  chap_state *cstate = (chap_state *) arg;
  
  /* if we aren't sending challenges, don't worry.  then again we */
  /* probably shouldn't be here either */
  if (cstate->clientstate != CHAPCS_CHALLENGE_SENT)
    return;
  
  ChapSendChallenge(cstate);			/* Send challenge */
  TIMEOUT(ChapTimeout, (caddr_t) cstate, cstate->timeouttime);
  ++cstate->retransmits;
}


/*
 * ChapLowerUp - The lower layer is up.
 *
 * Start up if we have pending requests.
 */
void
  ChapLowerUp(unit)
int unit;
{
  chap_state *cstate = &chap[unit];
  
  cstate->flags |= CHAPF_LOWERUP;
  if (cstate->flags & CHAPF_AWPPENDING)	/* were we attempting authwithpeer? */
    ChapAuthWithPeer(unit);	/* Try it now */
  if (cstate->flags & CHAPF_APPENDING)	/* or authpeer? */
    ChapAuthPeer(unit);
}


/*
 * ChapLowerDown - The lower layer is down.
 *
 * Cancel all timeouts.
 */
void
  ChapLowerDown(unit)
int unit;
{
  chap_state *cstate = &chap[unit];
  
  cstate->flags &= ~CHAPF_LOWERUP;
  
  if (cstate->clientstate == CHAPCS_CHALLENGE_SENT) /* Timeout pending? */
    UNTIMEOUT(ChapTimeout, (caddr_t) cstate);	/* Cancel timeout */
  
  if (cstate->serverstate == CHAPSS_OPEN) /* have we successfully authed? */
    LOGOUT(unit);
  cstate->clientstate = CHAPCS_CLOSED;
  cstate->serverstate = CHAPSS_CLOSED;
}


/*
 * ChapProtocolReject - Peer doesn't grok CHAP.
 */
void
  ChapProtocolReject(unit)
int unit;
{
  ChapLowerDown(unit);		/* shutdown chap */


/* Note: should we bail here if chap is required? */
}


/*
 * ChapInput - Input CHAP packet.
 */
void
  ChapInput(unit, inpacket, packet_len)
int unit;
u_char *inpacket;
int packet_len;
{
  chap_state *cstate = &chap[unit];
  u_char *inp;
  u_char code, id;
  int len;
  
  /*
   * Parse header (code, id and length).
   * If packet too short, drop it.
   */
  inp = inpacket;
  if (packet_len < CHAP_HEADERLEN) {
    CHAPDEBUG((LOG_INFO, "ChapInput: rcvd short header."))
    return;
  }
  GETCHAR(code, inp);
  GETCHAR(id, inp);
  GETSHORT(len, inp);
  if (len < CHAP_HEADERLEN) {
    CHAPDEBUG((LOG_INFO, "ChapInput: rcvd illegal length."))
    return;
  }
  if (len > packet_len) {
    CHAPDEBUG((LOG_INFO, "ChapInput: rcvd short packet."))
    return;
  }
  len -= CHAP_HEADERLEN;
  
  /*
   * Action depends on code.
   */
  switch (code) {
  case CHAP_CHALLENGE:
    ChapReceiveChallenge(cstate, inp, id, len);
    break;
    
  case CHAP_RESPONSE:
    ChapReceiveResponse(cstate, inp, id, len);
    break;
    
  case CHAP_FAILURE:
    ChapReceiveFailure(cstate, inp, id, len);
    break;

  case CHAP_SUCCESS:
    ChapReceiveSuccess(cstate, inp, id, len);
    break;

  default:				/* Need code reject? */
    syslog(LOG_WARNING, "Unknown CHAP code (%d) received.", code);
    break;
  }
}


/*
 * ChapReceiveChallenge - Receive Challenge.
 */
static void
  ChapReceiveChallenge(cstate, inp, id, len)
chap_state *cstate;
u_char *inp;
u_char id;
int len;
{
  u_char rchallenge_len;
  u_char *rchallenge;
  u_char secret[MAX_SECRET_LEN];
  int secret_len;
  u_char rhostname[256];
  u_char buf[256];
  MD5_CTX mdContext;
 
  CHAPDEBUG((LOG_INFO, "ChapReceiveChallenge: Rcvd id %d.", id))
  if (cstate->serverstate != CHAPSS_LISTEN) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveChallenge: received challenge but not in listen state"))
    return;
  }
  
  if (len < 2) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveChallenge: rcvd short packet."))
    return;
  }
  GETCHAR(rchallenge_len, inp);
  len -= sizeof (u_char) + rchallenge_len ;
  if (len < 0) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveChallenge: rcvd short packet."))
    return;
  }
  rchallenge = inp;
  INCPTR(rchallenge_len, inp);

  BCOPY(inp, rhostname, len);
  rhostname[len] = '\000';

  CHAPDEBUG((LOG_INFO, "ChapReceiveChallenge: received name field: %s",
	     rhostname))
  GETSECRET(rhostname, secret, &secret_len);/* get secret for specified host */

  BCOPY(rchallenge, buf, rchallenge_len); /* copy challenge into buffer */
  BCOPY(secret, buf + rchallenge_len, secret_len); /* append secret */

  /*  generate MD based on negotiated type */

  switch (lcp_hisoptions[cstate->unit].chap_mdtype) { 

  case CHAP_DIGEST_MD5:		/* only MD5 is defined for now */
    MD5Init(&mdContext);
    MD5Update(&mdContext, buf, rchallenge_len + secret_len);
    MD5Final(&mdContext); 
    ChapSendResponse(cstate, id, &mdContext.digest[0], MD5_SIGNATURE_SIZE);
    break;

  default:
    CHAPDEBUG((LOG_INFO, "unknown digest type %d",
	       lcp_hisoptions[cstate->unit].chap_mdtype))
  }

}


/*
 * ChapReceiveResponse - Receive and process response.
 */
static void
  ChapReceiveResponse(cstate, inp, id, len)
chap_state *cstate;
u_char *inp;
u_char id;
int len;
{
  u_char *remmd, remmd_len;
  u_char secret[MAX_SECRET_LEN];
  int secret_len;
  u_char chal_len = cstate->chal_len;
  u_char code;
  u_char rhostname[256];
  u_char buf[256];
  MD5_CTX mdContext;
  u_char msglen;
  char msg[256];

  CHAPDEBUG((LOG_INFO, "ChapReceiveResponse: Rcvd id %d.", id))

/* sanity check */
  if (cstate->clientstate != CHAPCS_CHALLENGE_SENT) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveResponse: received response but did not send a challenge"))
    return;
  }
  
  if (len < 2) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveResponse: rcvd short packet."))
    return;
  }
  GETCHAR(remmd_len, inp);		/* get length of MD */
  len -= sizeof (u_char) + remmd_len ;

  if (len < 0) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveResponse: rcvd short packet."))
    return;
  }

  remmd = inp;			/* get pointer to MD */
  INCPTR(remmd_len, inp);

  BCOPY(inp, rhostname, len);
  rhostname[len] = '\000';

  CHAPDEBUG((LOG_INFO, "ChapReceiveResponse: received name field: %s",
	     rhostname))

  GETSECRET(rhostname, secret, &secret_len);/* get secret for specified host */

  BCOPY(cstate->chal_str, buf, chal_len); /* copy challenge */
						  /* into buffer */ 
  BCOPY(secret, buf + chal_len, secret_len); /* append secret */

  /*  generate MD based on negotiated type */

  switch (lcp_gotoptions[cstate->unit].chap_mdtype) { 

  case CHAP_DIGEST_MD5:		/* only MD5 is defined for now */
    MD5Init(&mdContext);
    MD5Update(&mdContext, buf, chal_len + secret_len);
    MD5Final(&mdContext); 

    /* compare local and remote MDs and send the appropriate status */

    if (memcmp (&mdContext.digest[0], remmd, MD5_SIGNATURE_SIZE)) 
      code = CHAP_FAILURE;	/* they ain't the same */
    else
      code = CHAP_SUCCESS;	/* they are the same! */
    break;

    default:
      CHAPDEBUG((LOG_INFO, "unknown digest type %d",
		 lcp_gotoptions[cstate->unit].chap_mdtype))
    }
    if (code == CHAP_SUCCESS)
      sprintf(msg, "Welcome to %s.", hostname);
    else
      sprintf(msg, "I don't like you.  Go 'way.");
    msglen = strlen(msg);
    ChapSendStatus(cstate, code, id, (u_char *)msg, msglen);
  
  /* only crank up IPCP when either we aren't doing PAP, or if we are, */
  /* that it is in open state */

    if (code == CHAP_SUCCESS) {
      cstate->serverstate = CHAPSS_OPEN;
      if (!lcp_hisoptions[cstate->unit].neg_upap ||
	  (lcp_hisoptions[cstate->unit].neg_upap &&
	   upap[cstate->unit].us_serverstate == UPAPSS_OPEN ))
	ipcp_activeopen(cstate->unit);	/* Start IPCP */
    }
}
/*
 * ChapReceiveSuccess - Receive Success
 */
/* ARGSUSED */
static void
  ChapReceiveSuccess(cstate, inp, id, len)
chap_state *cstate;
u_char *inp;
u_char id;
int len;
{
  u_char msglen;
  u_char *msg;

  CHAPDEBUG((LOG_INFO, "ChapReceiveSuccess: Rcvd id %d.", id))

  if (cstate->clientstate != CHAPCS_CHALLENGE_SENT) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveSuccess: received success, but did not send a challenge."))
    return;
  }

  /*
   * Parse message.
   */
  if (len < sizeof (u_char)) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveSuccess: rcvd short packet."))
    return;
  }
  GETCHAR(msglen, inp);
  len -= sizeof (u_char);
  if (len < (int)msglen) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveSuccess: rcvd short packet."))
    return;
  }
  msg = inp;
  PRINTMSG(msg, msglen);
  
  cstate->clientstate = CHAPCS_OPEN;

  /* only crank up IPCP when either we aren't doing PAP, or if we are, */
  /* that it is in open state */

  if (!lcp_gotoptions[cstate->unit].neg_chap ||
      (lcp_gotoptions[cstate->unit].neg_chap &&
       upap[cstate->unit].us_serverstate == UPAPCS_OPEN ))
    ipcp_activeopen(cstate->unit);	/* Start IPCP */
}


/*
 * ChapReceiveFailure - Receive failure.
 */
/* ARGSUSED */
static void
  ChapReceiveFailure(cstate, inp, id, len)
chap_state *cstate;
u_char *inp;
u_char id;
int len;
{
  u_char msglen;
  u_char *msg;
  
  CHAPDEBUG((LOG_INFO, "ChapReceiveFailure: Rcvd id %d.", id))
  if (cstate->clientstate != CHAPCS_CHALLENGE_SENT) /* XXX */
    return;
  
  /*
   * Parse message.
   */
  if (len < sizeof (u_char)) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveFailure: rcvd short packet."))
    return;
  }
  GETCHAR(msglen, inp);
  len -= sizeof (u_char);
  if (len < (int)msglen) {
    CHAPDEBUG((LOG_INFO, "ChapReceiveFailure: rcvd short packet."))
    return;
  }
  msg = inp;
  PRINTMSG(msg, msglen);
  
  cstate->flags &= ~CHAPF_UPVALID;	/* Clear valid flag */
  cstate->clientstate = CHAPCS_CLOSED;	/* Pretend for a moment */
  ChapAuthWithPeer(cstate->unit);	/* Restart */
}


/*
 * ChapSendChallenge - Send an Authenticate challenge.
 */
static void
  ChapSendChallenge(cstate)
chap_state *cstate;
{
  u_char *outp;
  u_char chal_len;
  int outlen;

/* pick a random challenge length between MIN_CHALLENGE_LENGTH and 
   MAX_CHALLENGE_LENGTH */  
  cstate->chal_len =  (unsigned) ((drand48() *
			   (MAX_CHALLENGE_LENGTH - MIN_CHALLENGE_LENGTH)) +
			  MIN_CHALLENGE_LENGTH);
  chal_len = cstate->chal_len;

  outlen = CHAP_HEADERLEN + 2 * sizeof (u_char) + chal_len + hostname_len;
  outp = outpacket_buf;

  MAKEHEADER(outp, CHAP);		/* paste in a CHAP header */
  
  PUTCHAR(CHAP_CHALLENGE, outp);
  PUTCHAR(++cstate->id, outp);
  PUTSHORT(outlen, outp);

  PUTCHAR(chal_len, outp);	/* put length of challenge */

  ChapGenChallenge(chal_len, cstate->chal_str); /* generate a challenge string */

  BCOPY(cstate->chal_str, outp, chal_len); /* copy it the the output buffer */
  INCPTR(chal_len, outp);

  BCOPY(hostname, outp, hostname_len); /* append hostname */
  INCPTR(hostname_len, outp);

  output(cstate->unit, outpacket_buf, outlen + DLLHEADERLEN);
  
  CHAPDEBUG((LOG_INFO, "ChapSendChallenge: Sent id %d.", cstate->id))
  cstate->clientstate |= CHAPCS_CHALLENGE_SENT;
}


/*
 * ChapSendStatus - Send a status response (ack or nak).
 */
static void
  ChapSendStatus(cstate, code, id, msg, msglen)
chap_state *cstate;
u_char code, id;
u_char *msg;
int msglen;
{
  u_char *outp;
  int outlen;
  
  outlen = CHAP_HEADERLEN + msglen;
  outp = outpacket_buf;

  MAKEHEADER(outp, CHAP);	/* paste in a header */
  
  PUTCHAR(code, outp);
  PUTCHAR(id, outp);
  PUTSHORT(outlen, outp);
  BCOPY(msg, outp, msglen);
  output(cstate->unit, outpacket_buf, outlen + DLLHEADERLEN);
  
  CHAPDEBUG((LOG_INFO, "ChapSendStatus: Sent code %d, id %d.", code, id))
}

/*
 * ChapGenChallenge is used to generate a pseudo-random challenge string of
 * a pseudo-random length between min_len and max_len and return the
 * challenge string, and the message digest of the secret appended to
 * the challenge string.  the message digest type is specified by mdtype.
 *
 * It returns with the string in the caller-supplied buffer str (which
 * should be instantiated with a length of max_len + 1), and the
 * length of the generated string into chal_len.
 *
 */

static void
  ChapGenChallenge(chal_len, str)
u_char chal_len;
u_char * str;
{
  u_char * ptr = str;
  unsigned int i;

  /* generate a random string */

  for (i = 0; i < chal_len; i++ )
    *ptr++ = (char) (drand48() * 0xff);
      
  *ptr = 0;		/* null terminate it so we can printf it */
}
/*
 * ChapSendResponse - send a response packet with the message
 *                      digest specified by md and md_len
 */
/* ARGSUSED */
static void
  ChapSendResponse(cstate, id, md, md_len)
chap_state *cstate;
u_char id;
u_char *md;
int md_len;
{
    u_char *outp;
    int outlen;

    outlen = CHAP_HEADERLEN + sizeof (u_char) + md_len + hostname_len;
    outp = outpacket_buf;
    MAKEHEADER(outp, CHAP);

    PUTCHAR(CHAP_RESPONSE, outp); /* we are a response */
    PUTCHAR(id, outp);		/* copy id from challenge packet */
    PUTSHORT(outlen, outp);	/* packet length */

    PUTCHAR(md_len, outp);	/* length of MD */

    BCOPY(md, outp, md_len);	/* copy MD to buffer */
    INCPTR(md_len, outp);

    BCOPY(hostname, outp, hostname_len); /* append hostname */
    INCPTR(hostname_len, outp);

    output(cstate->unit, outpacket_buf, outlen + DLLHEADERLEN); /* bomb's away! */
}
