/*
 * Provide support routines for common authenticator components.  The
 * cache routines provide a cache of username/password pairs for quick
 * lookup (allowing several pages in a row to benefit from the same
 * UAF lookup).
 *
 * Author: David Jones
 * Date:   1-JUN-2000
 * Revised: 22-JUN-2000		Fix bug in auth_update_pwd_cache
 * Revised: 2-JUL-2000		Fix lib$sig_to_ret declaration.
 */
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <descrip.h>		/* VMS string descriptors */
#include <ssdef.h>		/* VMS sys. service return codes */
#include <prvdef.h>		/* VMS privilege bits */
#include "ctype_np.h"
#include <uaidef.h>		/* VMS GETUAI codes */
#include "md5.h"		/* Eric Young's MD5 digester */
int sys$bintim(), sys$gettim(), lib$subx(), sys$setprv();
int sys$getuai(), sys$hash_password();
typedef struct { int l; char *s; } string;

#include "authlib.h"		/* for auth_getuai_context() function */
#include "authutil.h"		/* verify prototypes in header */
/*
 * Define structures for caching sysuaf data.
 */
static int uaf_htbl_size;
static int uaf_cache_limit = 0;
static int cache_allocated = 0;
static long cache_timeout[2];

static struct UAFINFO {
    struct UAFINFO *next;
    long expiration[2];
    char username[40];
    char password[40];
} **uaf_hashtbl, *uaf_free;
/*
 * Define structures for intrusion scan.
 */
static $DESCRIPTOR(iss_user_dx,"");
static long priv_mask[2], prev_privs[2];
static long *uai_context;
/*
 * Structures for external authentication.
 */
static int ext_auth_loaded;
static int ext_auth_flags;
static int ext_auth_version;
static int ext_auth_tag_limit;		/* size of ext_auth_tags table */
static char **ext_auth_tags;
int (*check_ext_pwd) ( char *user, char *pass, char *domain,
	char **table, unsigned char *remaddr, int addr_fmt  );
/*
 * Hash function for determining starting hash table position.
 */
static int hash_name(char *s)
{
    char c;
    int i = 0;
    int mask;

    while (c = *s++) {
        i = ((i>>24)&0x3f) ^ c ^ (i<<6);
    }
    return i;
}
/****************************************************************************/
/* Do garbage collection (move expired cached entries to free list), if
 * free list after GC is empty, allocate more cache blocks for it.
 */
static int expand_uaf_free ( int alloc_size ) {
    int i, j, expired;
    long delta[2], cur_time[2];
    struct UAFINFO *cb, *prev_cb;
    /*
     * Check each hash table chain for expired entries.
     */
    sys$gettim ( cur_time );
    for ( i = 0; i < uaf_htbl_size; i++ ) if ( uaf_hashtbl[i] ) {
	prev_cb = (struct UAFINFO *) 0;
	for ( cb = uaf_hashtbl[i]; cb; ) {
	    /* 
	     * See if cur_time is larger (after) cache block's expiration.
	     */
	    lib$subx ( cb->expiration, cur_time, delta );
	    expired = (delta[1] < 0);
	    if ( expired ) {
		/*
		 * Remove block from hash chain and add to free list.
		 */
		/* fprintf(stderr, "Removing expired entry for cache, %s\n", cb->username); */
		for ( j = 0; cb->password[j]; j++ ) cb->password[j] = '\0';
		if ( prev_cb ) prev_cb->next = cb->next;
		else uaf_hashtbl[i] = cb->next;
		cb->next = uaf_free;
		uaf_free = cb;

		cb = (prev_cb) ? prev_cb->next : uaf_hashtbl[i];
	    } else {
		prev_cb = cb;
	        cb = cb->next;
	    }
	}
    }
    /*
     * Expand free list by alloc_size blocks if still emtpy and alloc_size > 0.
     * Limit total number of cache blocks allocated to uaf_cache_limit.
     */
    if ( uaf_free ) return 1;
    if ( alloc_size > (uaf_cache_limit - cache_allocated) )
	alloc_size = uaf_cache_limit - cache_allocated;
    if ( alloc_size <= 0 ) return 0;
    /*
     * Allocate all in 1 chunk and divide up into linked list.
     */
    cb = uaf_free;			/* Current free list */
    uaf_free = malloc ( sizeof(struct UAFINFO) * alloc_size );
    if ( !uaf_free ) return 0;
    cache_allocated += alloc_size;
    for ( i = 0; i < (alloc_size-1); i++ ) uaf_free[i].next = &uaf_free[i+1];
    uaf_free[alloc_size-1].next = cb;	/* Append previous free list to end */
    return 1;
}

/****************************************************************************/
/* Initialize the password cache.  The routine must only be called once.
 */
int auth_init_vms_pwd ( 
	char *iss_user,
	long *installed_privs,		/* mask of available privs */
	int htbl_size,			/* entries in hash table (power of 2) */
	int limit,			/* max number of entries */
	char *timeout )			/* Delta time (e.g. 1:00:00) */
{
    int i, status;
    static $DESCRIPTOR(timeout_str,"");
    /*
     * Save copy of iss_user to be used as source user in ISS scans.
     */
    iss_user_dx.dsc$w_length = strlen ( iss_user );
    iss_user_dx.dsc$a_pointer = malloc ( iss_user_dx.dsc$w_length + 1 );
    strcpy ( iss_user_dx.dsc$a_pointer, iss_user );
    /*
     * Save the installed privs mask for checking for security privilege
     * and initialize mask of privileges we must enable to do SYSUAF lookups.
     */
    prev_privs[0] = installed_privs[0];
    prev_privs[1] = installed_privs[1];
    priv_mask[0] = PRV$M_SYSPRV;
    priv_mask[1] = 0;
    uai_context = auth_getuai_context();
    ext_auth_loaded = 0;
    /*
     * Make the htble_size a power of 2, allocate and initialize the table.
     */
    cache_allocated = 0;
    uaf_free = (struct UAFINFO *) 0;

    if ( htbl_size == 0 ) htbl_size = 256;
    for ( uaf_htbl_size = 2; htbl_size > uaf_htbl_size; uaf_htbl_size *= 2 );
    uaf_hashtbl = (struct UAFINFO **) malloc ( uaf_htbl_size *
	sizeof(struct UAFINFO *) );
    if ( !uaf_hashtbl ) return 0;		/* malloc failure */

    for (i = 0; i < uaf_htbl_size; i++) uaf_hashtbl[i] = (struct UAFINFO *) 0;
    /*
     * Pre-allocate the first 40 blocks in the cache.
     */
    uaf_cache_limit = limit;
    expand_uaf_free(40);
    /*
     * Convert the timeout string into a VMS binary time.
     */
    timeout_str.dsc$w_length = strlen ( timeout );
    timeout_str.dsc$a_pointer = timeout;
    status = sys$bintim ( &timeout_str, cache_timeout );
    if ( 0 == (status&1) ) {
	fprintf(stderr,"Bad cache timeout specified (%s)\n", timeout );
    }

    return status;
}
/****************************************************************************/
/* Dyanmically load shareable image that contains external authorization
 * checks.  The shareable image named must contains entry points for the
 * fololowing functions:
 *
 *    int INITIALIZE();
 *    int CHECK_PASSWORD ( char *username, char *password );
 */
int auth_load_ext_handler ( 
	char *image_name, 		/* Shareable image name */
	int *flags, int *version,
	char ***taglist, int *tlist_size )
{
    static $DESCRIPTOR ( image_dir, "WWW_SYSTEM:.exe" );
    static $DESCRIPTOR ( image_dx, "" );
    static $DESCRIPTOR ( init_symbol, "INITIALIZE" );
    static $DESCRIPTOR ( check_symbol, "CHECK_PASSWORD" );
    int status, i, LIB$FIND_IMAGE_SYMBOL();
    unsigned int LIB$SIG_TO_RET( void *, void * );
    int (*init_routine)();

    if ( !image_name ) return SS$_BADPARAM;
    image_dx.dsc$w_length = strlen ( image_name );
    image_dx.dsc$a_pointer = image_name;

    VAXC$ESTABLISH( LIB$SIG_TO_RET ); /* suppress tracebacks on failure */

    status = LIB$FIND_IMAGE_SYMBOL ( &image_dx, &init_symbol, &init_routine,
	&image_dir );
    if ( (status&1) == 1 ) {
	status = LIB$FIND_IMAGE_SYMBOL ( &image_dx, &check_symbol, 
		&check_ext_pwd, &image_dir );
	if ( (status&1) == 1 ) {
	    /*
	     * Both symbols found in image, call the init routine.
	     * If init routine returns error, ignore the load.
	     */
	    status = init_routine ( &ext_auth_flags, &ext_auth_version,
		&ext_auth_tags, &ext_auth_tag_limit );
	    if ((status&1) == 0 ) fprintf (stderr,
		"Initialize call on %s returned failed.\n", image_name );
	}
    }
    /*
     * Relay information returned by external image to caller.
     */
    if ( flags ) *flags = ext_auth_flags;
    if ( version ) *version = ext_auth_version;
    if ( taglist ) *taglist = ext_auth_tags;
    if ( tlist_size ) *tlist_size = ext_auth_tag_limit;
    if ( (status&1) == 1 ) ext_auth_loaded = 1;
    return status;
}

/****************************************************************************/
/*
 * Define wrapper function for calling $scan_intrusion system service.
 * Lack of SECURITY privilege disables use of $scan_intrustion.
 */
#ifdef HAS_SCAN_INTRUSION
#include <ciadef>
#include <jpidef>
#include <secsrvdef>
#ifdef PRV$M_SECURITY
#undef PRV$M_SECURITY
#endif
#define PRV$M_SECURITY 64
#ifdef VAXC
globalref int secsrv$_intruder;
#else
extern int secsrv$_intruder;
#endif

static int scan_intrusion ( int login_status, int username_valid,
	struct dsc$descriptor_s *username, unsigned char *remote_addr )
{
    int status, sys$scan_intrusion(), flags;
    $DESCRIPTOR(remote_node_dx, "");
    char remote_node[20];
    /*
     * Check that process has SECURITY privilege.
     */
    if ( (PRV$M_SECURITY&prev_privs[1]) == 0 ) return login_status;
    /*
     * Register activity. Encode remote address as node name and
     * use MD5_AUTHENTICATOR as source user.
     */
    sprintf(remote_node,"%d.%d.%d.%d", remote_addr[0], remote_addr[1],
	remote_addr[2], remote_addr[3] );
    remote_node_dx.dsc$w_length = strlen ( remote_node );
    remote_node_dx.dsc$a_pointer = remote_node;
    flags = (username_valid) ? CIA$M_REAL_USERNAME : 0;

    status = sys$scan_intrusion ( login_status, username, JPI$K_NETWORK,
	0, &remote_node_dx, &iss_user_dx, 0, 0, 0, 0, flags );
    /*
     * Fail request if intruder detected, otherwise return original
     * login status.
     */
    if ( status == (int) &secsrv$_intruder ) return 0;

    return (1&login_status);
}
#else
/*
 * Skip using SYS$SCAN_INTRUSION completely (VMS 5.x)
 */
#define scan_intrusion(a,b,c,d) (a)
#endif
/***************************************************************************/
/* Primary routine for validating a username/password pair against the VMS
 * authorization database.  The valid passwords are cached for future
 * calls.
 */
int auth_check_vms_password ( 
	char *username, 		/* username to test against */
	char *password, 		/* password to test */
	unsigned char *remote_address,	/* IP address of source */
	int addr_format )		/* must be zero */
{
    int iss, hndx, status, length, i, password_correct;
    unsigned long hash1[2], hash0[2];
    struct auth_pwd_ctx cache_ctx;
    long cur_time[2], delta[2];
    static long context = -1;
    static char user_buf[256];
    static char pass_buf[256];
    static $DESCRIPTOR(d_user,user_buf);
    static $DESCRIPTOR(d_pass,pass_buf);
    unsigned char algorithm;
    unsigned short salt;
    unsigned long flags, len0, len1, len2, prev_mask[2];

    struct item_list_3 {
        unsigned short il3$w_buflen;
        unsigned short il3$w_code;
        void *         il3$a_buffer;
        unsigned long *il3$a_retlen;
    };

    static struct item_list_3 uai_items[]  = {
        {  8, UAI$_PWD, NULL, NULL},    /* &hash0[0], &len0}, */
        {  2, UAI$_SALT, NULL, NULL},   /* &salt,     &len1}, */
        {  1, UAI$_ENCRYPT, NULL, NULL},/* &algorithm,&len2}, */
	{  4, UAI$_FLAGS, NULL, NULL},
        {  0, 0, NULL, NULL}
    };
    struct UAFINFO *cb;
    /*
     * Check cache for matching entry and return immediately if match.
     */
    if ( username[0] == '\0' ) return 0;		/* null username */

    status = auth_check_pwd_cache ( username, password, &cache_ctx );
#ifdef DEBUG
    printf("status of cache check: %d, usr: %d pwd: %d exp: %d, ndx: %d\n", 
	status, cache_ctx.username_match,cache_ctx.password_match,
	cache_ctx.expired, cache_ctx.hndx );
#endif
    if ( (status&1) == 0 ) return 0;			/* severe error */

    if ( cache_ctx.username_match && cache_ctx.password_match ) return 1;
    /*
     * Not in cache (or expired), validate username against SYSUAF.
     */
    uai_items[0].il3$a_buffer = &hash0[0];
    uai_items[0].il3$a_retlen = &len0;
    uai_items[1].il3$a_buffer = &salt;
    uai_items[1].il3$a_retlen = &len1;
    uai_items[2].il3$a_buffer = &algorithm;
    uai_items[2].il3$a_retlen = &len2;
    uai_items[3].il3$a_buffer = &flags;

    length = strlen ( username );
    if ( length >= sizeof(user_buf) ) length = sizeof(user_buf) - 1;
    for ( i = 0; i < length; i++ ) user_buf[i] = _toupper(username[i]);
    user_buf[length] = '\0';
    d_user.dsc$w_length  = length;
    d_pass.dsc$a_pointer = password;
    length = strlen ( password );
    if ( length >= sizeof(pass_buf) ) length = sizeof(pass_buf) - 1;
    d_pass.dsc$w_length  = strlen(password);
    /*
     * Enable SYSPRV temporarily, get UAF items and disable again.
     */
    iss = sys$setprv ( 1, priv_mask, 0, prev_mask );
    if (!(iss&1)) return 0;

    iss = sys$getuai(0,uai_context,&d_user,uai_items,0,0,0);

    prev_mask[0] = (~prev_mask[0]) & priv_mask[0];
    prev_mask[1] = (~prev_mask[1]) & priv_mask[1];
    if ( prev_mask[0] || prev_mask[1] ) {
	/* Disable the privileges we enabled that were previously disabled */
    	(void) sys$setprv ( 0, prev_mask, 0, 0 );
    }

    if ( 0 == (iss&1) ) {
	/* Register failure with intrusion database */
	iss = scan_intrusion ( iss, 0, &d_user, remote_address );
    }
    password_correct = 0;
    /*
     * Verify the password.
     */
    if ( (iss&1) == 1 ) {
#ifdef UAI$M_EXTAUTH
	if ( ((flags&UAI$M_EXTAUTH) != 0) && ext_auth_loaded ) {
	    /*
	     * Call out to external routine to perform the authentication check.
	     * Setting domain argument to null indicates this is an 'ext. auth'
	     * SYSUAF check rather than a non-VMS username.
	     */
	    password_correct = check_ext_pwd ( username, password, (char *) 0,
		(char **) 0, remote_address, addr_format );
	} else {
#else
	{
#endif
	    /*
	     * Hash the password and compare with the UAF entry.
	     */
	    d_pass.dsc$a_pointer = pass_buf;
	    for ( i = 0; i < d_pass.dsc$w_length; i++ ) {
	        pass_buf[i] = _toupper(password[i]);
	    }
	    pass_buf[d_pass.dsc$w_length] = '\0';
	    iss = sys$hash_password(&d_pass,algorithm,salt,&d_user,&hash1[0]);
	    memset ( pass_buf, 0, d_pass.dsc$w_length );
	    if ( (hash0[0] == hash1[0]) && (hash0[1] == hash1[1]) )
		password_correct = 1;
	}
    }
    if (!(iss&1) ) {
	/* system service failure */
	return 0;
    }
    /*
     * At this point, we have tested the user's password and have his SYSUAF
     * entry.
     */
    if ( ((flags&UAI$M_DISACNT) == 0 ) && password_correct ) {
	/*
	 * Verify user was not flagged an intruder.
	 */
	iss = scan_intrusion ( 1, 1, &d_user, remote_address );
	if ( 0 == (iss&1) ) return 0;
	/* 
	 * Password validated, update the cache.
	 */
	cache_ctx.expired = 0;
        auth_update_pwd_cache ( &cache_ctx );
	status = 1;

    } else {
	/*
	 * Tell VMS about the failure.
	 */
	iss = scan_intrusion ( 0, 0, &d_user, remote_address );
	status = 0;
    }

    return status;
}

/****************************************************************************/
/* Lookup the cache block for a username and check the password.
 *
 * Checking a password in the cache has the following possible
 * outcomes:
 *    0		Username not found in cache.
 *    1		Username found, password matches and is valid.
 *    2		Username found, password matches but is stale.
 *    3		Username found, password does not match.
 */
int auth_check_pwd_cache ( 
	char *username,			/* Key for cache entry */
	char *password,
        struct auth_pwd_ctx *ctx )	/* result */
{
    struct UAFINFO *cb, *prev_cb;
    int j;
    long cur_time[2], delta[2];
    /*
     * Initialize the context.
     */
    ctx->username_match = 0;
    ctx->password_match = 0;
    ctx->expired = 0;
    ctx->cand_username = username;
    ctx->cand_password = password;
    ctx->hndx = (uaf_htbl_size-1)&hash_name(username);
    ctx->cb = (void *) 0;
    /*
     * Search the chain of cache blocks pointed to by the hash table entry.
     */
    for ( cb = uaf_hashtbl[ctx->hndx]; cb; cb = cb->next ) {
	if ( 0 == strcmp ( username, cb->username ) ) {
	    /*
	     * Found cache block with matching username, check if expired
	     * and if password matches.
	     */
	    ctx->username_match = 1;		/* found username */
	    sys$gettim ( cur_time );
	    lib$subx ( cb->expiration, cur_time, delta );
	    ctx->expired = (delta[1] < 0);
	    if ( !ctx->expired ) {
		/*
		 * Only check password on non-stale entries.
		 */
		if ( 0 == strcmp ( password, cb->password ) )
			ctx->password_match = 1;
	    }
	    ctx->cb = (void *) cb;
	    break;
	}
    }
    return 1;
}
/*
 * The update function either adds a new entry to the cache or updates
 * the expiration time and password of an existing entry or deletes an
 * expired entry depending upon the flags in the context structure.
 */
int auth_update_pwd_cache ( struct auth_pwd_ctx *ctx )
{
    struct UAFINFO *cb, *prev_cb;
    long cur_time[2];

    cb = (struct UAFINFO *) ctx->cb;
    if ( !cb ) {
	/*
	 * Allocate new cache block and link into hash chain.
	 */
	if ( !uaf_free ) if ( !expand_uaf_free ( 100 ) ) return 1;
	cb = uaf_free;
	uaf_free = cb->next;
	ctx->cb = (void *) cb;
	cb->next = uaf_hashtbl[ctx->hndx];
	uaf_hashtbl[ctx->hndx] = cb;

    } else if ( ctx->expired ) {
	/*
	 * The cache entry is stale, zero password and put on free list.
	 */
	int i;
	expand_uaf_free ( 0 );
	ctx->cb = (void *) 0;
	return 1;
    }
    /*
     * Update the cache block values.
     */
    sys$gettim ( cur_time );
    lib$subx ( cur_time, cache_timeout, cb->expiration );
    strncpy ( cb->username, ctx->cand_username, sizeof(cb->username)-1 );
    strncpy ( cb->password, ctx->cand_password, sizeof(cb->password)-1 );
    cb->username[sizeof(cb->username)-1] = '\0';
    cb->password[sizeof(cb->password)-1] = '\0';

    return 1;
}
/*************************************************************************/
/* Generate www-authenticate header for MD5 digest authentication style.
 * Output buffer must be null-terminated and include ending carriage control.
 */
void auth_digest_challenge ( char *header, int hdr_size, char *realm, char *uri )
{
    char nonce_val[33];
    nonce_val[32] = '\0';
    sprintf ( header,
	"WWW-Authenticate: digest realm=\"%s\", %s%s%s%s\r\n",
	realm, "nonce=\"", auth_digest_md5(nonce_val, "%s", uri ),
	"\", algorithm=\"MD5\", stale=\"false\"", " opaque=\"aabc\"" );
}
/*************************************************************************/
/* Return 32 character hex encoding of formatted string.  Construction
 * of string to digest is controlled by fmt string which contains conversion
 * codes introduced by a percent sign (e.g. %s),  Codes are:
 *
 *    % - Insert % in data stream.
 *    s - Take next argument as pointer to zero-terminated ascii string.
 *    m - take next argument as pointer to 32 raw data bytes (another MD5 output.
 *    q - take next 2 arguments as a byte count (int) followed by
 *        pointer to count number of raw data bytes.
 */
char *auth_digest_md5 ( char *result,	/* 32 char array to receive result */
	char *fmt,			/* format control string */
	... )				/* variable number of arguments. */
{
    int i, j, length;
    va_list ap;				/* argument pointer */
    char *arg;
    MD5_CTX c;
    unsigned char digest[MD5_BLOCK];

    MD5_Init ( &c );
    va_start(ap, fmt);			/* point to first variable argument */
    /*
     * Scan for format string and build MD5 checksum from the components.
     */
    for ( i = j = 0; fmt[i]; i++ ) if ( fmt[i] == '%' ) {
	i++;
	if ( !fmt[i] ) break;		/* dangling percent is illegal. */
	if ( fmt[i] == '%' ) {
	    MD5_Update(&c, (unsigned char *) &fmt[j], i-j );
	} else if ( fmt[i] == 's' ) {
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    arg = va_arg(ap, char *);
	    length = strlen ( arg );
	    MD5_Update ( &c, (unsigned char *) arg, length );
	} else if ( fmt[i] == 'm' ) {
	    /*
	     * String points to 32 character hash.
	     */
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    arg = va_arg(ap, char *);
	    MD5_Update ( &c, (unsigned char *) arg, 32 );
	} else if ( fmt[i] == 'q' ) {
	    /*
	     * String points to counted string.
	     */
	    int bcount;
	    if ((i-j) > 1) MD5_Update ( &c, (unsigned char *) &fmt[j], i-j-1 );
	    bcount = va_arg(ap, int);
	    arg = va_arg(ap, char *);
	    MD5_Update ( &c, (unsigned char *) arg, bcount );
	}
	j = i+1;			/* start of next segment */
    }

    if ( j < i ) MD5_Update ( &c, (unsigned char *) &fmt[j], i - j );
    MD5_Final ( digest, &c );
    /*
     * Convert final 128 bit digest to hex result.
     */
    for ( i = j = 0; i < MD5_BLOCK; i++ ) {
	int nibble;
	nibble = (digest[i]>>4)&15;
	if ( nibble < 10 ) result[j++] = '0' + nibble;
	else result[j++] = 'a' + (nibble-10);
	nibble = (digest[i])&15;
	if ( nibble < 10 ) result[j++] = '0' + nibble;
	else result[j++] = 'a' + (nibble-10);
    }
    return result;    
}
/****************************************************************************/
/* Make callout to dynamically loaded password checker routine.
 */
int auth_check_ext_password (
	char *username,
	char *password,
	char *domain,
	char **tag_table,
	unsigned char *remote_addr,
	int addr_format )
{
    int status;

    if ( !ext_auth_loaded ) return 0;		/* no extension available */
    status = check_ext_pwd ( username, password, domain,
	tag_table, remote_addr, addr_format );
    return status;
}
