/*
 * Kerberos V5 authentication scheme
 *
 * Author: Tom Yu <tlyu@MIT.EDU>
 *
 * Mostly snarfed wholesale from the user_user demo in the
 * krb5 distribution. (At least the checking part)
 */
#include <sys/types.h>
#include <sys/socket.h>
#ifdef TCPCONN
#include <netinet/in.h>
#endif
#ifdef DNETCONN
#include <netdnet/dn.h>
#endif
#include <arpa/inet.h>
#include <krb5/krb5.h>
#include <krb5/los-proto.h>
#include "X.h"
#include "os.h"
#include "osdep.h"
#include "Xproto.h"
#include "Xfuncs.h"
#include "dixstruct.h"
#include "k5encode.h"
#include <com_err.h>
#include "Xauth.h"

extern int (*k5_Vector[256])();
extern int (*InitialVector[3])();
extern int SendConnSetup();
extern char *display;		/* need this to generate rcache name */

static XID krb5_id = ~0L;
static krb5_principal srvname = NULL; /* service name */
static char *ccname = NULL;
static char *ktname = NULL;	/* key table name */
static char kerror[256];

/*
 * tgt_keyproc:
 *
 * extract session key from a credentials struct
 */
krb5_error_code tgt_keyproc(keyprocarg, principal, vno, key)
    krb5_pointer keyprocarg;
    krb5_principal principal;
    krb5_kvno vno;
    krb5_keyblock **key;
{
    krb5_creds *creds = (krb5_creds *)keyprocarg;
    
    return krb5_copy_keyblock(&creds->keyblock, key);
}

/*
 * k5_cmpenc:
 *
 * compare "encoded" principals
 */
Bool k5_cmpenc(pname, plen, buf)
    unsigned char *pname;
    short plen;
    krb5_data *buf;
{
    return (plen == buf->length &&
	    bcmp(pname, buf->data, plen) == 0);
}

/*
 * K5Check:
 *
 * This is stage 0 of the krb5 authentication protocol.  It
 * goes through the current credentials cache and extracts the
 * primary principal and tgt to send to the client, or as
 * appropriate, extracts from a keytab.
 *
 * The packet sent to the client has the following format:
 *
 * CARD8	reqType	= 2
 * CARD8	data	= 0
 * CARD16	length	= total length of packet (in 32 bit units)
 * CARD16	plen	= length of encoded principal following
 * STRING8	princ	= encoded principal
 * STRING8	ticket	= server tgt
 *
 * For client-server authentication, the packet is as follows:
 *
 * CARD8	reqType	= 3
 * CARD8	data	= 0
 * CARD16	length	= total length
 * STRING8	princ	= encoded principal of server
 */
XID K5Check(data_length, data, client)
    unsigned short data_length;
    char *data;
    ClientPtr client;
{
    krb5_error_code retval;
    CARD16 tlen;
    krb5_principal sprinc, cprinc;
    krb5_ccache cc;
    krb5_creds *creds;
    char *outbuf, *cp;
    krb5_data princ;
    register char n;
    xReq prefix;

    if (krb5_id == ~0L)
	return ~0L;
    if (!ccname && !srvname)
	return ~0L;
    if (ccname)
    {
	if ((creds = (krb5_creds *)malloc(sizeof(krb5_creds))) == NULL)
	    return ~0L;
	if (retval = krb5_cc_resolve(ccname, &cc))
	    return ~0L;
	bzero((char*)creds, sizeof (krb5_creds));
	if (retval = krb5_cc_get_principal(cc, &cprinc))
	{
	    krb5_free_creds(creds);
	    krb5_cc_close(cc);
	    return ~0L;
	}
	creds->client = cprinc;
	if (retval =
	    krb5_build_principal_ext(&sprinc, 
				     krb5_princ_realm(creds->client)->length,
				     krb5_princ_realm(creds->client)->data,
				     6, "krbtgt",
				     krb5_princ_realm(creds->client)->length,
				     krb5_princ_realm(creds->client)->data,
				     0))
	{
	    krb5_free_creds(creds);
	    krb5_cc_close(cc);
	    return ~0L;
	}
	creds->server = sprinc;
	retval = krb5_get_credentials(KRB5_GC_CACHED, cc, creds);
	krb5_cc_close(cc);
	if (retval)
	{
	    krb5_free_creds(creds);
	    return ~0L;
	}
	if (retval = k5_encode(cprinc, &princ))
	{
	    krb5_free_creds(creds);
	    return ~0L;
	}
	tlen = sz_xReq + 2 + princ.length + creds->ticket.length;
	prefix.reqType = 2;	/* opcode = authenticate user-to-user */
    }
    else if (srvname)
    {
	if (retval = k5_encode(srvname, &princ))
	{
	    return ~0L;
	}
	tlen = sz_xReq + princ.length;
	prefix.reqType = 3;	/* opcode = authenticate client-server */
    }
    prefix.data = 0;		/* stage = 0 */
    prefix.length = (tlen + 3) >> 2; /* round up to nearest multiple
					of 4 bytes */
    if (client->swapped)
    {
	swaps(&prefix.length, n);
    }
    if ((cp = outbuf = (char *)malloc(tlen)) == NULL)
    {
	if (ccname)
	{
	    krb5_free_creds(creds);
	}
	free(princ.data);
	return ~0L;
    }
    bcopy((char *)&prefix, cp, sz_xReq);
    cp += sz_xReq;
    if (ccname)
    {
	bcopy((char *)&princ.length, cp, 2);
	if (client->swapped)
	{
	    swaps((CARD16 *)cp, n);
	}
	cp += 2;
    }
    bcopy(princ.data, cp, princ.length);
    cp += princ.length;
    free(princ.data);		/* we don't need that anymore */
    if (ccname)
	bcopy(creds->ticket.data, cp, creds->ticket.length);
    WriteToClient(client, tlen, outbuf);
    free(outbuf);
    client->requestVector = k5_Vector; /* hack in our dispatch vector */
    if (ccname)
    {
	client->authstate.srvcreds = (pointer)creds; /* save tgt creds */
	client->authstate.ktname = NULL;
	client->authstate.srvname = NULL;
    }
    if (srvname)
    {
	client->authstate.srvcreds = NULL;
	client->authstate.ktname = (pointer)ktname;
	client->authstate.srvname = (pointer)srvname;
    }
    client->authstate.stageno = 1; /* next stage is 1 */
    return krb5_id;
}

/*
 * k5_stage1:
 *
 * This gets called out of the dispatcher after K5Check frobs with the
 * client->requestVector.  It accepts the ap_req from the client and verifies
 * it.  In addition, if the client has set AP_OPTS_MUTUAL_REQUIRED, it then
 * sends an ap_rep to the client to achieve mutual authentication.
 *
 * client stage1 packet format is as follows:
 *
 * CARD8	reqType	= 1
 * CARD8	data	= ignored
 * CARD16	length	= total length
 * STRING8	data	= the actual ap_req
 *
 * stage2 packet sent back to client for mutual authentication:
 *
 * CARD8	reqType	= 2
 * CARD8	data	= 2
 * CARD16	length	= total length
 * STRING8	data	= the ap_rep
 */
int k5_stage1(client)
    register ClientPtr client;
{
    long addrlen;
    krb5_error_code retval, retval2;
    register char n;
    struct sockaddr cli_net_addr;
    xReq prefix;
    krb5_principal cprinc;
    krb5_data buf;
    krb5_creds *creds = (krb5_creds *)client->authstate.srvcreds;
    krb5_keyblock *skey;
    krb5_address cli_addr, **localaddrs = NULL;
    krb5_tkt_authent *authdat;
    krb5_ap_rep_enc_part rep;
    krb5_int32 ctime, cusec;
    krb5_rcache rcache = NULL;
    char *cachename = NULL, *rc_type = NULL, *rc_base = "rcX", *kt = NULL;
    REQUEST(xReq);

    if (client->authstate.stageno != 1)
    {
	client->requestVector = InitialVector;
	if (creds)
	    krb5_free_creds(creds);
	return(SendConnSetup(client, "expected stage1 packet"));
    }
    addrlen = sizeof (cli_net_addr);
    if (getpeername(((OsCommPtr)client->osPrivate)->fd,
		    &cli_net_addr, &addrlen) == -1)
    {
	client->requestVector = InitialVector;
	if (creds)
	    krb5_free_creds(creds);
	return(SendConnSetup(client, "getpeername failed"));
    }
    if (cli_net_addr.sa_family == AF_UNSPEC ||
#ifdef UNIXCONN
	cli_net_addr.sa_family == AF_UNIX
#endif
	)			/* assume local host */
    {
	krb5_os_localaddr(&localaddrs);
	if (!localaddrs || !localaddrs[0])
	{
	    client->requestVector = InitialVector;
	    if (creds)
		krb5_free_creds(creds);
	    return(SendConnSetup(client, "failed to get localaddrs"));
	}
	cli_addr.addrtype = localaddrs[0]->addrtype;
	cli_addr.length = localaddrs[0]->length;
	cli_addr.contents = localaddrs[0]->contents;
    }
    else
    {
	cli_addr.addrtype = cli_net_addr.sa_family; /* the values
						       are compatible */
	switch (cli_net_addr.sa_family)
	{
#ifdef TCPCONN
	case AF_INET:
	    cli_addr.length = sizeof (struct in_addr);
	    cli_addr.contents =
		(krb5_octet *)&((struct sockaddr_in *)&cli_net_addr)->sin_addr;
	    break;
#endif
#ifdef DNETCONN
	case AF_DECnet:
	    cli_addr.length = sizeof (struct dn_naddr);
	    cli_addr.contents =
		(krb5_octet *)&((struct sockaddr_dn *)&cli_net_addr)->sdn_add;
	    break;
#endif
	default:
	    client->requestVector = InitialVector;
	    if (localaddrs)
		krb5_free_addresses(localaddrs);
	    if (creds)
		krb5_free_creds(creds);
	    return(SendConnSetup(client, "bogus addrtype from getpeername"));
	}
    }
    if ((rcache = (krb5_rcache)malloc(sizeof(*rcache))) == NULL)
    {
	client->requestVector = InitialVector;
	if (localaddrs)
	    krb5_free_addresses(localaddrs);
	if (creds)
	    krb5_free_creds(creds);
	return(SendConnSetup(client, "malloc bombed for rcache"));
    }
    if ((rc_type = krb5_rc_default_type()) == NULL)
	rc_type = "dfl";
    if (retval = krb5_rc_resolve_type(&rcache, rc_type))
    {
	client->requestVector = InitialVector;
	if (localaddrs)
	    krb5_free_addresses(localaddrs);
	if (creds)
	    krb5_free_creds(creds);
	free(rcache);
	strcpy(kerror, "rc_resolve_type failed: ");
	strncat(kerror, error_message(retval), 231);
	return(SendConnSetup(client, kerror));
    }
    if ((cachename = (char *)malloc(strlen(rc_base) + strlen(display) + 1))
	== NULL)
    {
	client->requestVector = InitialVector;
	if (localaddrs)
	    krb5_free_addresses(localaddrs);
	if (creds)
	    krb5_free_creds(creds);
	free(rcache);
	return(SendConnSetup(client, "malloc bombed for cachename"));
    }
    strcpy(cachename, rc_base);
    strcat(cachename, display);
    if (retval = krb5_rc_resolve(rcache, cachename))
    {
	client->requestVector = InitialVector;
	if (localaddrs)
	    krb5_free_addresses(localaddrs);
	if (creds)
	    krb5_free_creds(creds);
	free(rcache);
	free(cachename);
	strcpy(kerror, "rc_resolve failed: ");
	strncat(kerror, error_message(retval), 236);
	return(SendConnSetup(client, kerror));
    }
    free(cachename);
    if (krb5_rc_recover(rcache))
    {
	extern krb5_deltat krb5_clockskew;
	if (retval = krb5_rc_initialize(rcache, krb5_clockskew))
	{
	    client->requestVector = InitialVector;
	    if (localaddrs)
		krb5_free_addresses(localaddrs);
	    if (creds)
		krb5_free_creds(creds);
	    if (retval2 = krb5_rc_close(rcache))
	    {
		client->requestVector = InitialVector;
		strcpy(kerror, "rc_close failed: ");
		strncat(kerror, error_message(retval2), 238);
		return(SendConnSetup(client, kerror));
	    }
	    free(rcache);
	    strcpy(kerror, "rc_initialize failed: ");
	    strncat(kerror, error_message(retval), 233);
	    return(SendConnSetup(client, kerror));
	}
    }
    buf.length = (stuff->length << 2) - sz_xReq;
    buf.data = (char *)stuff + sz_xReq;
    if (creds)
    {
	retval = krb5_rd_req(&buf,
			     NULL, /* don't bother with server name */
			     &cli_addr,
			     NULL, /* no fetchfrom */
			     tgt_keyproc,
			     creds, /* credentials as arg to
				       keyproc */
			     rcache,
			     &authdat);
	krb5_free_creds(creds);
    }
    else if (kt = (char *)client->authstate.ktname)
    {
	retval = krb5_rd_req(&buf, srvname, &cli_addr, kt, NULL, NULL,
			     rcache, &authdat);
	client->authstate.ktname = NULL;
    }
    else
    {
	client->requestVector = InitialVector;
	if (localaddrs)
	    krb5_free_addresses(localaddrs);
	return(SendConnSetup(client, "neither srvcreds nor ktname set"));
    }
    if (localaddrs)
	krb5_free_addresses(localaddrs);
    if (rcache)
    {
	if (retval2 = krb5_rc_close(rcache))
	{
	    client->requestVector = InitialVector;
	    strcpy(kerror, "rc_close failed: ");
	    strncat(kerror, error_message(retval2), 238);
	    return(SendConnSetup(client, kerror));
	}
	free(rcache);
    }
    if (retval)
    {
	client->requestVector = InitialVector;
	strcpy(kerror, "krb5_rd_req failed: ");
	strncat(kerror, error_message(retval), 235);
	return(SendConnSetup(client, kerror));
    }
    cprinc = authdat->ticket->enc_part2->client;
    skey = authdat->ticket->enc_part2->session;
    if (retval = k5_encode(cprinc, &buf))
    {
	krb5_free_tkt_authent(authdat);
	client->requestVector = InitialVector;
	return(SendConnSetup(client, "k5_encode bombed"));
    }
    /*
     * Now check to see if the principal we got is one that we want to let in
     */
    if (ForEachHostInFamily(FamilyKrb5Principal, k5_cmpenc, (pointer)&buf))
    {
	free(buf.data);
	/*
	 * The following deals with sending an ap_rep to the client to
	 * achieve mutual authentication.  The client sends back a stage 3
	 * packet if all is ok.
	 */
	if (authdat->ap_options | AP_OPTS_MUTUAL_REQUIRED)
	{
	    /*
	     * stage 2: send ap_rep to client
	     */
	    if (retval = krb5_us_timeofday(&ctime, &cusec))
	    {
		krb5_free_tkt_authent(authdat);
		client->requestVector = InitialVector;
		strcpy(kerror, "error in us_timeofday: ");
		strncat(kerror, error_message(retval), 234);
		return(SendConnSetup(client, kerror));
	    }
	    rep.ctime = ctime;
	    rep.cusec = cusec;
	    rep.subkey = NULL;
	    rep.seq_number = 0;
	    if (retval = krb5_mk_rep(&rep, skey, &buf))
	    {
		krb5_free_tkt_authent(authdat);
		client->requestVector = InitialVector;
		strcpy(kerror, "error in mk_rep: ");
		strncat(kerror, error_message(retval), 238);
		return(SendConnSetup(client, kerror));
	    }
	    prefix.reqType = 2;	/* opcode = authenticate */
	    prefix.data = 2;	/* stage = 2 */
	    prefix.length = (buf.length + sz_xReq + 3) >> 2;
	    if (client->swapped)
	    {
		swaps(&prefix.length, n);
	    }
	    WriteToClient(client, sz_xReq, (char *)&prefix);
	    WriteToClient(client, buf.length, buf.data);
	    free(buf.data);
	    krb5_free_tkt_authent(authdat);
	    client->authstate.stageno = 3; /* expect stage3 packet */
	    return(Success);
	}
	else
	{
	    free(buf.data);
	    krb5_free_tkt_authent(authdat);
	    return(SendConnSetup(client, NULL)); /* success! */
	}
    }
    else
    {
	krb5_free_tkt_authent(authdat);
	client->requestVector = InitialVector;
	free(buf.data);
	return(SendConnSetup(client,
		"principal is not authorized to connect to server"));
    }
}

/*
 * k5_stage3:
 *
 * Get the short ack packet from the client.  This packet can conceivably
 * be expanded to allow for switching on end-to-end encryption.
 *
 * stage3 packet format:
 *
 * CARD8	reqType	= 3
 * CARD8	data	= ignored (for now)
 * CARD16	length	= should be zero
 */
int k5_stage3(client)
    register ClientPtr client;
{
    REQUEST(xReq);

    if (client->authstate.stageno != 3)
    {
	client->requestVector = InitialVector;
	return(SendConnSetup(client, "expected stage3 packet!"));
    }
    else
	return(SendConnSetup(client, NULL)); /* success! */
}

k5_bad(client)
    register ClientPtr client;
{
    client->requestVector = InitialVector;
    if (client->authstate.creds)
	krb5_free_creds((krb5_creds *)client->authstate.creds);
    return(SendConnSetup(client, "unrecognized auth packet"));
}

/*
 * K5Add:
 *
 * Takes the name of a credentials cache and resolves it.  Also adds the
 * primary principal of the ccache to the acl.
 *
 * Now will also take a service name.
 */
int K5Add(data_length, data, id)
    unsigned short data_length;
    char *data;
    XID id;
{
    krb5_principal princ;
    krb5_error_code retval;
    krb5_keytab_entry tmp_entry;
    krb5_keytab keytab;
    krb5_kvno kvno = 0;
    krb5_ccache cc;
    char *nbuf, *cp;
    krb5_data kbuf;
    int i, ktlen;
    
    krb5_init_ets();		/* can't think of a better place to put it */
    krb5_id = ~0L;
    if (data_length < 3)
	return 0;
    if ((nbuf = (char *)malloc(data_length - 2)) == NULL)
	return 0;
    bcopy(data + 3, nbuf, data_length - 3);
    nbuf[data_length - 3] = '\0';
    if (ccname)
    {
	free(ccname);
	ccname = NULL;
    }
    if (srvname)
    {
	krb5_free_principal(srvname);
	srvname = NULL;
    }
    if (ktname)
    {
	free(ktname);
	ktname = NULL;
    }
    if (!strncmp(data, "UU:", 3))
    {
	if (retval = krb5_cc_resolve(nbuf, &cc))
	{
	    free(nbuf);
	    return 0;
	}
	if (cc && !(retval = krb5_cc_get_principal(cc, &princ)))
	{
	    if (k5_encode(princ, &kbuf))
	    {
		free(nbuf);
		krb5_free_principal(princ);
		krb5_cc_close(cc);
		return 0;
	    }
	    if (krb5_cc_close(cc))
		return 0;
	    AddHost(NULL, FamilyKrb5Principal, kbuf.length, kbuf.data);
	    krb5_free_principal(princ);
	    free(kbuf.data);
	    ccname = nbuf;
	    krb5_id = id;
	    return 1;
	}
    }
    else if (!strncmp(data, "CS:", 3))
    {
	if ((cp = strchr(nbuf, ',')) == NULL)
	{
	    free(nbuf);
	    return 0;
	}
	*cp = '\0';		/* gross but it works :-) */
	ktlen = strlen(cp + 1);
	if ((ktname = (char *)malloc(ktlen + 1)) == NULL)
	{
	    free(nbuf);
	    return 0;
	}
	strcpy(ktname, cp + 1);
	retval = krb5_sname_to_principal(NULL, /* NULL for hostname uses
						  local host name*/
					 nbuf, KRB5_NT_SRV_HST,
					 &srvname);
	free(nbuf);
	if (retval)
	{
	    free(ktname);
	    ktname = NULL;
	    return 0;
	}
	if (retval = krb5_kt_resolve(ktname, &keytab))
	{
	    free(ktname);
	    ktname = NULL;
	    krb5_free_principal(srvname);
	    srvname = NULL;
	    return 0;
	}
	retval = krb5_kt_get_entry(keytab, srvname, kvno, &tmp_entry);
	krb5_kt_free_entry(&tmp_entry);
	if (retval)
	{
	    free(ktname);
	    ktname = NULL;
	    krb5_free_principal(srvname);
	    srvname = NULL;
	    return 0;
	}
	if (k5_encode(srvname, &kbuf))
	{
	    free(ktname);
	    ktname = NULL;
	    krb5_free_principal(srvname);
	    srvname = NULL;
	    return 0;
	}
	AddHost(NULL, FamilyKrb5Principal, kbuf.length, kbuf.data);
	krb5_id = id;
	return 1;
    }
    return 0;
}

/*
 * K5Reset:
 *
 * Reset krb5_id, also nuke the current principal from the acl.
 */
int K5Reset()
{
    krb5_principal princ;
    krb5_error_code retval;
    krb5_ccache cc;
    krb5_data kbuf;
    int i;
    
    if (ccname)
    {
	if (retval = krb5_cc_resolve(ccname, &cc))
	{
	    free(ccname);
	    ccname = NULL;
	}
	if (cc && !(retval = krb5_cc_get_principal(cc, &princ)))
	{
	    if (k5_encode(princ, &kbuf))
		return 1;
	    RemoveHost(NULL, FamilyKrb5Principal, kbuf.length, kbuf.data);
	    krb5_free_principal(princ);
	    free(kbuf.data);
	    if (krb5_cc_close(cc))
		return 1;
	    free(ccname);
	    ccname = NULL;
	}
    }
    if (srvname)
    {
	if (k5_encode(srvname, &kbuf))
	    return 1;
	RemoveHost(NULL, FamilyKrb5Principal, kbuf.length, kbuf.data);
	krb5_free_principal(srvname);
	free(kbuf.data);
	srvname = NULL;
    }
    if (ktname)
    {
	free(ktname);
	ktname = NULL;
    }
    krb5_id = ~0L;
    return 0;
}

XID K5ToID(data_length, data)
    unsigned short data_length;
    char *data;
{
    return krb5_id;
}

int K5FromID(id, data_lenp, datap)
    XID id;
    unsigned short *data_lenp;
    char **datap;
{
    return 0;
}

int K5Remove(data_length, data)
    unsigned short data_length;
    char *data;
{
    return 0;
}
