/* sample2.c -- sample custom CRAM-MD5 authentication routine
 */

#if defined(__sun) && !defined(_REENTRANT)
#define _REENTRANT
#endif

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <pwd.h>
#include <md5.h>
#include <netdb.h>
#include <errno.h>
#include "authserv.h"

/* IMPORTANT: don't use global data unless read-only or protected by a mutex
 */

static char *mymechlist[] = { "CRAM-MD5", (char *)0 };

/* header to announce version & mechanisms supported
 */
static struct authhead ahead = {
    AUTHHEAD_ID,
    AUTHHEAD_VERSION,
    mymechlist
};

/* counter & mutex for CRAM-MD5 challenge
 */
static int gcounter = 1;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/* storage for fully-qualified Internet host name of server
 */
static char hostname[1024];

/* this is used to initialize global context for the custom authentication
 * routine.  See authserv.h for more details.
 */
int authdat_init(void **context)
{
    int err, h_err;
    struct hostent *hent, hbuf;
    char *hname, **halias;
    char buf[4096];
    
    *context = (void *)&ahead;

    /* attempt to determine our fully qualified Internet domain name */
    err = gethostname(hostname, sizeof (hostname));
    if (err != 0) {
	fprintf(stderr, "Couldn't determine host name: %s (%d)\n",
		strerror(errno), errno);
	return (-1);
    }
    if (strchr(hostname, '.') != (char *)0) {
	return (0);
    }
    hent = gethostbyname_r(hostname, &hbuf, buf, sizeof(buf), &h_err);
    if (hent == (struct hostent *)0) {
	fprintf(stderr, "Couldn't determine Internet host name for '%s': %d\n",
		hostname, h_err);
	return (-1);
    }
    hname = hent->h_name;
    if (strchr(hname, '.') == (char *)0) {
	halias = hent->h_aliases;
	if (halias != (char **)0) {
	    for (; *halias != (char *)0; ++halias) {
		if (strchr(*halias, '.') != (char *)0) {
		    hname = *halias;
		    break;
		}
	    }
	}
	if (halias == (char **)0 || *halias == (char *)0) {
	    fprintf(stderr, "Couldn't determine Internet host name for '%s'\n",
		    hname);
	    return (-1);
	}
    }
    strlcpy(hostname, hname, sizeof (hostname));

    return (0);
}

/* MD5 hash output size and internal block size
 */
#define HASH_SIZE  16
#define BLOCK_SIZE 64

/* routine to calculate CRAM-MD5 hash value
 *  outbuf     Output buffer with store for at least HASH_SIZE * 2 + 1
 *  password   Input password
 *  challenge  Input Challenge
 */
static void cram_md5_calc(char *outbuf, const char *password,
			  const char *challenge)
{
    static const char hexval[] = "0123456789abcdef";
    MD5_CTX md5c;
    int pwlen, i;
    unsigned char k_pad[BLOCK_SIZE];
    unsigned char digest[HASH_SIZE];

    pwlen = strlen(password);

    /* If password is too long, hash it */
    if (pwlen > BLOCK_SIZE) {
	MD5Init(&md5c);
	MD5Update(&md5c, (unsigned char *) password, pwlen);
	MD5Final(k_pad, &md5c);
	password = (const char *) k_pad;
	pwlen = HASH_SIZE;
    }

    /* HMAC-MD5; XOR padded key with inner pad value */
    for (i = 0; i < pwlen; i++) {
	k_pad[i] = (unsigned char)(password[i] ^ 0x36);
    }
    if (i < BLOCK_SIZE) {
	memset(k_pad + i, 0x36, BLOCK_SIZE - i);
    }

    /* Compute inner hash */
    MD5Init(&md5c);
    MD5Update(&md5c, k_pad, BLOCK_SIZE);
    MD5Update(&md5c, (unsigned char *)challenge, strlen(challenge));
    MD5Final(digest, &md5c);

    /* XOR padded key with outer pad value */
    for (i = 0; i < BLOCK_SIZE; ++i) {
	k_pad[i] ^= (0x36 ^ 0x5c);
    }

    /* Compute outer hash */
    MD5Init(&md5c);
    MD5Update(&md5c, k_pad, BLOCK_SIZE);
    MD5Update(&md5c, digest, HASH_SIZE);
    MD5Final(digest, &md5c);

    /* Hex encode result */
    for (i = 0; i < HASH_SIZE; ++i) {
	outbuf[i*2]   = hexval[digest[i] >> 4];
	outbuf[i*2+1] = hexval[digest[i] & 0xf];
    }
    outbuf[HASH_SIZE * 2] = '\0';

    /* clean up workspaces */
    memset(k_pad, 0, BLOCK_SIZE);
    memset(digest, 0, HASH_SIZE);
}

/* this is the thread-safe authentication handler
 */
void authdat_handler(void *context, const struct authdata *adat)
{
    struct replydata rdat;
    int err = SASL_NOUSER;
    int counter, chlen;
    struct memobj *mobj = (struct memobj *) adat->authcontinue;
    char *split;
    char hval[HASH_SIZE * 2 + 1];

    (void)context; /* unused */
    
    /* only support CRAM-MD5 authentication */
    if (strcmp(adat->saslmech, "CRAM-MD5") != 0) {
	adat->auth_fail(adat, SASL_NOMECH, NULL, NULL);
	return;
    }

    /* make sure we have empty reply data */
    memset(&rdat, 0, sizeof (rdat));
    rdat.version = REPLAYDATA_VERSION;

    /* if no challenge sent yet, send it now */
    if (mobj == (struct memobj *)0) {
	/* make sure client is following protocol */
	if (adat->sasllen != 0) {
	    adat->auth_fail(adat, SASL_BADPROT, 0, 0);
	    return;
	}
	/* allocate space for challenge */
	chlen = strlen(hostname) + 37;
	mobj = (struct memobj *) malloc(sizeof (struct memobj) + chlen);
	if (mobj == (struct memobj *)0) {
	    adat->auth_fail(adat, SASL_NOMEM, 0, 0);
	    return;
	}
	mobj->destruct = free;

	/* generate unique challenge */
	pthread_mutex_lock(&mutex);
	counter = gcounter++;
	pthread_mutex_unlock(&mutex);
	rdat.sasllen = snprintf(mobj->data, chlen, "<%x.%x@%s>",
				getpid(), counter, hostname);
	rdat.sasldata = mobj->data;
	rdat.authcontinue = mobj;
	adat->auth_success(adat, &rdat);
	return;
    }

    /* Must have data from client */
    if (adat->sasllen < HASH_SIZE * 2 + 2 || adat->sasldata == (char *)0) {
	adat->auth_fail(adat, SASL_BADPROT, 0, "CRAM-MD5 response too short");
	return;
    }

    /* Parse client data (note: it is permissible to alter data) */
    split = strrchr(adat->sasldata, ' ');
    if (split == (char *)0) {
	adat->auth_fail(adat, SASL_BADPROT, 0, "CRAM-MD5 missing delimiter");
	return;
    }
    *split = '\0';

    /* validate the user name. For this example, only accept one user */
    if (strcasecmp(adat->sasldata, "cnewman") != 0) {
	/* important to distinguish error code for user not found from
	 * error code for bad password.  But text messages reported to
	 * client should be the same for both by default.
	 */
	adat->auth_fail(adat, SASL_NOUSER, 0, 0);
	return;
    }

    /* compute CRAM-MD5 hash from the challenge and password */
    cram_md5_calc(hval, "password", mobj->data);

    /* validate password */
    if (strcmp(split + 1, hval) != 0) {
	adat->auth_fail(adat, SASL_BADAUTH, 0, 0);
	return;
    }

    /* success (note: it is permissible to return pointer to sasldata) */
    rdat.orig_user = adat->sasldata;
    adat->auth_success(adat, &rdat);

    /* NOTE: memory object is automatically disposed */
}
