/* This file is part of the Project Athena Zephyr Notification System.
 * It contains source for the ZCheckAuthentication function.
 *
 *	Created by:	Robert French
 *
 *	$Source$
 *	$Author$
 *
 *	Copyright (c) 1987,1991 by the Massachusetts Institute of Technology.
 *	For copying and distribution information, see the file
 *	"mit-copyright.h".
 */
/* $Header$ */

#ifndef lint
static const char rcsid_ZCheckAuthentication_c[] =
    "$Id: 690097172694e4ad7e3e9501d8e2b9ed3a3e6121 $";
#endif

#include <internal.h>

#if defined(HAVE_KRB5) && !HAVE_KRB5_FREE_DATA
#define krb5_free_data(ctx, dat) free((dat)->data)
#endif

/* Check authentication of the notice.
   If it looks authentic but fails the Kerberos check, return -1.
   If it looks authentic and passes the Kerberos check, return 1.
   If it doesn't look authentic, return 0

   When not using Kerberos, return true if the notice claims to be authentic.
   Only used by clients; the server uses its own routine.
 */
#ifdef HAVE_KRB5
static Code_t Z_CheckZcodeAuthentication(ZNotice_t *notice,
					 struct sockaddr_in *from,
					 krb5_keyblock *keyblock)
{
    krb5_error_code result;
    krb5_enctype enctype;
    krb5_cksumtype cksumtype;
    krb5_data cksumbuf;
    int valid;
    char *cksum0_base, *cksum1_base = NULL, *cksum2_base;
    char *x;
    unsigned char *asn1_data, *key_data, *cksum_data;
    int asn1_len, key_len, cksum0_len = 0, cksum1_len = 0, cksum2_len = 0;

    /* Figure out what checksum type to use */
    key_data = Z_keydata(keyblock);
    key_len = Z_keylen(keyblock);
    result = Z_ExtractEncCksum(keyblock, &enctype, &cksumtype);
    if (result) {
	return (ZAUTH_FAILED);
    }

    /* Assemble the things to be checksummed */
    /* first part is from start of packet through z_default_format:
     * - z_version
     * - z_num_other_fields
     * - z_kind
     * - z_uid
     * - z_port
     * - z_auth
     * - z_authent_len
     * - z_ascii_authent
     * - z_class
     * - z_class_inst
     * - z_opcode
     * - z_sender
     * - z_recipient
     * - z_default_format
     */
    cksum0_base = notice->z_packet;
    x           = notice->z_default_format;
    cksum0_len  = x + strlen(x) + 1 - cksum0_base;
    /* second part is from z_multinotice through other fields:
     * - z_multinotice
     * - z_multiuid
     * - z_sender_(sock)addr
     * - z_charset
     * - z_other_fields[]
     */
    if (notice->z_num_hdr_fields > 15 ) {
	cksum1_base = notice->z_multinotice;
	if (notice->z_num_other_fields)
	    x = notice->z_other_fields[notice->z_num_other_fields - 1];
	else  {
	    /* see also server/kstuff.c:ZCheck{Realm,}Authentication */
	    /* XXXXXXXXXXXXXXXXXXXXXXX */
	    if (notice->z_num_hdr_fields > 16)
		x = cksum1_base + strlen(cksum1_base) + 1; /* multinotice */
	    if (notice->z_num_hdr_fields > 17)
		x = x + strlen(x) + 1; /* multiuid */
	    if (notice->z_num_hdr_fields > 18)
		x = x + strlen(x) + 1; /* sender */
	}
	cksum1_len  = x + strlen(x) + 1 - cksum1_base; /* charset / extra field */
    }

    /* last part is the message body */
    cksum2_base = notice->z_message;
    cksum2_len  = notice->z_message_len;

    /* The following code checks for old-style checksums, which will go
       away once Kerberos 4 does. */
    if ((!notice->z_ascii_checksum || *notice->z_ascii_checksum != 'Z') &&
	key_len == 8 &&
	(enctype == (krb5_enctype)ENCTYPE_DES_CBC_CRC ||
	 enctype == (krb5_enctype)ENCTYPE_DES_CBC_MD4 ||
	 enctype == (krb5_enctype)ENCTYPE_DES_CBC_MD5)) {
	/* try old-format checksum (covers cksum0 only) */

	ZChecksum_t our_checksum;

	our_checksum = z_quad_cksum((unsigned char *)cksum0_base, NULL, cksum0_len, 0,
				    key_data);
	if (our_checksum == notice->z_checksum) {
	    return ZAUTH_YES;
	}
    }

    cksumbuf.length = cksum0_len + cksum1_len + cksum2_len;
    cksumbuf.data = malloc(cksumbuf.length);
    if (!cksumbuf.data) {
	return ZAUTH_NO;
    }
    /* HOLDING: cksumbuf.data */

    cksum_data = (unsigned char *)cksumbuf.data;
    memcpy(cksum_data, cksum0_base, cksum0_len);
    if (cksum1_len)
	memcpy(cksum_data + cksum0_len, cksum1_base, cksum1_len);
    memcpy(cksum_data + cksum0_len + cksum1_len,
	   cksum2_base, cksum2_len);

    /* decode zcoded checksum */
    /* The encoded form is always longer than the original */
    asn1_len = strlen(notice->z_ascii_checksum) + 1;
    asn1_data = malloc(asn1_len);
    if (!asn1_data) {
	free(cksumbuf.data);
	return ZAUTH_FAILED;
    }
    /* HOLDING: asn1_data, cksumbuf.data */
    result = ZReadZcode((unsigned char *)notice->z_ascii_checksum,
			asn1_data, asn1_len, &asn1_len);
    if (result != ZERR_NONE) {
	free(asn1_data);
	free(cksumbuf.data);
	return ZAUTH_FAILED;
    }
    /* HOLDING: asn1_data, cksumbuf.data */

    valid = Z_krb5_verify_cksum(keyblock, &cksumbuf, cksumtype,
				Z_KEYUSAGE_SRV_CKSUM, asn1_data, asn1_len);

    free(asn1_data);
    free(cksumbuf.data);

    if (valid)
	return ZAUTH_YES;
    else
	return ZAUTH_FAILED;
}
#endif

Code_t ZCheckZcodeAuthentication(ZNotice_t *notice,
				 struct sockaddr_in *from)
{
#ifdef HAVE_KRB5
    Code_t answer;
    krb5_creds *creds;
    struct _Z_SessionKey *savedkey, *todelete;
#endif

    /* If the value is already known, return it. */
    if (notice->z_checked_auth != ZAUTH_UNSET)
        return (notice->z_checked_auth);

    if (!notice->z_auth)
        return (ZAUTH_NO);

    if (!notice->z_ascii_checksum)
        return (ZAUTH_NO);

#ifdef HAVE_KRB5
    /* Try each of the saved session keys. */
    for (savedkey = Z_keys_head; savedkey != NULL; savedkey = savedkey->next) {
	answer = Z_CheckZcodeAuthentication(notice, from, savedkey->keyblock);
	if (answer == ZAUTH_YES) {
	    /* Save the time of the first use of each key. */
	    if (!savedkey->first_use) {
		savedkey->first_use = time(NULL);
	    } else {
		/*
		 * Any keys sent sufficiently long before this one is stale. If
		 * we know it has been long enough since the server learned of
		 * this key, we can prune keys made stale by this one.
		 */
		if (time(NULL) > savedkey->first_use + KEY_TIMEOUT) {
		    while (Z_keys_tail &&
			   Z_keys_tail->send_time + KEY_TIMEOUT < savedkey->send_time) {
			todelete = Z_keys_tail;
			Z_keys_tail = Z_keys_tail->prev;
			Z_keys_tail->next = NULL;

			krb5_free_keyblock(Z_krb5_ctx, todelete->keyblock);
			free(todelete);
		    }
		}
	    }
	    return answer;
	}
    }

    /*
     * If each of those fails, pull from the ccache. This is to preserve the
     * behavior of things like zwgc/zctl where another program actually
     * generates the subscription notices.
     */
    if (ZGetCreds(&creds))
	return ZAUTH_NO;

    answer = Z_CheckZcodeAuthentication(notice, from, Z_credskey(creds));

    krb5_free_creds(Z_krb5_ctx, creds);
    return answer;
#else
    return (notice->z_auth ? ZAUTH_YES : ZAUTH_NO);
#endif
}
