/*
 * $Id$
 *
 * Copyright (c) 1999 Andrew G. Morgan <morgan@linux.kernel.org>
 */

#include <unixdefs.h>
#include <dbierrno.h>
#include <dbiapi.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>

#include <security/pam_modules.h>
#include <security/pam_client.h>

/* #define DEBUG */
#include <security/_pam_macros.h>

#define AGENT_PREFIX "fp240x240@prototype1/"
#define RETVAL_FP240 "fp240x240@prototype-retval"
#define FP_FORMAT "%s/.ssh/fingerprint"

static DIT **free_dits(DIT **samples)
{
    int i;

    for (i=0; samples[i]; ++i) {
	free(samples[i]);
	samples[i] = 0;
    }

    free(samples);

    return NULL;
}

static DIT **load_sample_dits(const char *user)
{
    DIT **samples;
    struct passwd *pw;
    int i;
    int fd;
    unsigned char count;
    char *fpfile;

    pw = getpwnam(user);
    if (pw == NULL) {
	D(("user [%s] is not known", user));
	return NULL;
    }

    fpfile = malloc(sizeof(FP_FORMAT) + strlen(pw->pw_dir));
    sprintf(fpfile, FP_FORMAT, pw->pw_dir);
    pw = NULL;
    fd = open(fpfile, O_RDONLY);
    memset(fpfile, 0, strlen(fpfile));
    free(fpfile);

    if (fd < 0) {
	D(("failed to open fp file for user[%s]", user));
	return NULL;
    }

    if (read(fd, &count, 1) != 1) {
	D(("failed to read [%s] user's fingerprint file"));
	return NULL;
    }

    samples = calloc(count+1, sizeof(DIT *));
    if (samples == NULL) {
	D(("out of memory for list of DIT's"));
	return NULL;
    }

    /* loop and load all samples */
    for (i=0; i<count; ++i) {
	unsigned char len[2];
	int length, j=0;

	D(("loading sample %d", i));

	if (read(fd, len, 2) != 2) {
	    D(("problem reading fps from file"));
	    goto abort_load;
	}

	length = (len[0] << 8) + len[1];
	samples[i] = malloc(length);
	if (samples[i] == NULL) {
	    D(("failed to read sample %d", i));
	    goto abort_load;
	}

	D(("loading fp %d - %d", i, length));
	while (j<length) {
	    int raw = read(fd, j + (char *) samples[i], length-j);
	    D(("[%d]", raw));
	    if (raw <= 0) {
		D(("error reading fp %d [%d]", i, j));
		goto abort_load;
	    }

	    j += raw;
	}
	D(("loaded fp %d - %d", i, j));
    }

    close(fd);
    D(("done"));
    return samples;

 abort_load:

    close(fd);
    samples = free_dits(samples);
    D(("failed"));
    return NULL;
}

/* this function destroys the image - its no longer usable .. */
static int make_ddt(BYTE DEW_TYPE *image, DDT *ddt)
{
    int quality;
    BYTE DEW_TYPE *t1, *t2;

    memset(ddt, 0, DBI_sizeofDDT());

    t1 = calloc(1, IMAGE_WIDTH * IMAGE_HEIGHT);
    if (t1 == NULL) {
	return -1;
    }
    t2 = calloc(1, IMAGE_WIDTH * IMAGE_HEIGHT);
    if (t2 == NULL) {
	free(t1);
	t1 = NULL;
	return -1;
    }

#ifdef DEBUG
#ifdef PROGRAM
    {
	unsigned char *xx = (unsigned char *) image;
	int t=0;
	int i;
	for (i=0; i<(240*240); ++i) {
	    t = t*13 + ((unsigned char *) image)[i];
	}
	
	D(("raw checksum: %x [%2x%2x%2x%2x]", t, xx[0], xx[1], xx[2], xx[3]));
    }
#endif /* PROGRAM */
#endif /* DEBUG */

    {
	unsigned ex = DBI_CharacterizeImage(image, t1, t2, ddt, &quality);
	D(("ex = %x (%u) %p %p %p %x\n", ex, ex, t1, t2, ddt, quality));
    }

#ifdef DEBUG
#ifdef PROGRAM
    {
	unsigned char *xx = 100 + (unsigned char *) ddt;
	int t=0;
	int i;
	for (i=0; i<DBI_sizeofDDT(); ++i) {
	    t = t*13 + ((unsigned char *) ddt)[i];
	}
	D(("ddt checksum: %x", t));
	for (i=0; i<DBI_sizeofDDT(); ++i) {
	    D(("ddt checksum: [%3d:%2x]", i, *(i+xx)));
	}
    }
#endif /* PROGRAM */
#endif /* DEBUG */

    memset(t1, 0, IMAGE_WIDTH * IMAGE_HEIGHT);
    free(t1);
    t1 = NULL;
    memset(t2, 0, IMAGE_WIDTH * IMAGE_HEIGHT);
    free(t2);
    t2 = NULL;

    D(("quality = %x", quality));

    return quality;
}

static int authenticate_fp(pam_handle_t *pamh, pamc_bp_t bp)
{
    DDT *live=NULL, *official=NULL;
    DIT **dit_list;
    const char *user = NULL;
    unsigned char *cf;
    int quality, i, retval;

    D(("called"));

    pam_get_item(pamh, PAM_USER, (const void **) &user);
    if (user == NULL) {
	D(("what happened to the user?"));
	return PAM_USER_UNKNOWN;
    }

    /* should also have other methods for locating the user's fp. */

    dit_list = load_sample_dits(user);
    if (dit_list == NULL) {
	D(("no dit list available"));
	return PAM_AUTHINFO_UNAVAIL;
    }

    /*
     * Here, we compare the fingerprint data with the cached
     * version of the fingerprint.
     */

    /* Future versions of this module should enable the agent to return
       this fn argument information. In the meantime, it would appear
       that these numbers are the ones that the BioMouse returns */

    {
	double length;
	BYTE dark_field;

	DBI_InitDriver(&length, &dark_field);
	DBI_DLLInit(length, dark_field);
/*	DBI_DLLInit(0.4528, FALSE); */
    }

    live = malloc(DBI_sizeofDDT());
    official = malloc(DBI_sizeofDDT());
    if ((live == NULL) || (official == NULL)) {
	D(("no DDTs"));
	goto clean_up;
    }

    quality = make_ddt(PAM_BP_DATA(bp), live);
    switch (quality & 0xFF00) {
    case DBI_PRINT_NORMAL:
    case DBI_PRINT_DRY:
	D(("should be ok"));
	break;
    default:
	D(("we're not likely to get a good match from this"));
    }

    retval = PAM_AUTH_ERR;
    for (i=0; dit_list[i]; ++i) {
	long score, ret;

	D(("attempting to compare against %d sample", i));

	ret = DBI_UnCompress_DIT(dit_list[i], official);

	D(("uncompress -> %d", ret));

	if (ret != DBI_FAST_OK) {
	    D(("problem uncompressing dit %d", i));
	    goto clean_up;
	}

	score = DBI_Compare(official, live);

	/* need to be able to require a higher security level */
	if (DBI_Multi_Dim(&score, 1, FA_1_IN_500)) {
	    D(("good with score=%d", score));
	    retval = PAM_SUCCESS;
	    break;
	} else {
	    D(("bad [%d] with score=%d", i, score));
	}
    }

 clean_up:

    if (official) {
	memset(official, 0, DBI_sizeofDDT());
	free(official);
	official = NULL;
    }
    if (live) {
	memset(live, 0, DBI_sizeofDDT());
	free(live);
	live = NULL;
    }
    dit_list = free_dits(dit_list);

       DBI_Close();
   DBI_CloseDriver();

    D(("done %d", retval));
    return retval;
}

#if defined(PROGRAM)

static int fake_conv(int nmesg, const struct pam_message **msg,
		     struct pam_response **resp, void *appdata_ptr)
{
    int retval;
    pamc_handle_t pch = appdata_ptr;
    pamc_bp_t bp = NULL;

    if ((nmesg != 1) || (msg[0]->msg_style != PAM_BINARY_PROMPT)) {
	return PAM_CONV_ERR;
    }

    D(("here is the original prompt: [%s]", PAM_BP_DATA(msg[0]->msg)));

    PAM_BP_RENEW(&bp, PAM_BP_CONTROL(msg[0]->msg), PAM_BP_LENGTH(msg[0]->msg));
    PAM_BP_FILL(bp, 0, PAM_BP_LENGTH(msg[0]->msg), PAM_BP_DATA(msg[0]->msg));

    for (;;) {

	D(("here is the prompt: [%s]", PAM_BP_DATA(bp)));
	retval = pamc_converse(pch, &bp);
	D(("we've received a binary prompt of control(%u) and length(%u)",
	   PAM_BP_CONTROL(bp), PAM_BP_LENGTH(bp)));

	if (retval != PAM_BPC_TRUE) {
	    PAM_BP_RENEW(&bp, 0, 0);
	    D(("pamc_converse returned %d", retval));
	    return PAM_CONV_ERR;
	}

	if (PAM_BPC_FOR_CLIENT(bp)) {
	    D(("pamc_converse returned bp not for client %d",
	       PAM_BP_CONTROL(bp)));
	    break;
	}

	switch (PAM_BP_CONTROL(bp)) {
	case PAM_BPC_GETENV:       /* agent wants to know an environment
				      variable */
	{
	    const char *e;

	    e = getenv(PAM_BP_DATA(bp));
	    if (e) {
		PAM_BP_RENEW(&bp, PAM_BPC_OK, strlen(e));
		PAM_BP_FILL(bp, 0, strlen(e), e);
		e = NULL;
	    } else {
		PAM_BP_RENEW(&bp, PAM_BPC_FAIL, 0);
	    }

	    break;
	}

	default:
	    D(("not prepared to deal with agent request (%u)",
	       PAM_BP_CONTROL(bp)));
	    return PAM_CONV_ERR;
	}

    }

    *resp = calloc(1, sizeof(struct pam_response));
    (*resp)[0].resp = (char *) bp;

    return PAM_SUCCESS;
}

static struct pam_conv pc = {
    fake_conv,
    NULL
};

static char *user = NULL;
static const char *filename = NULL;
 
int pam_get_item(const pam_handle_t *pamh, int type, const void **dp)
{
    switch (type) {
    case PAM_USER:
	*dp = user;
	break;

    case PAM_CONV:
	*dp = &pc;
	break;

    default:
	fprintf(stderr, "unsupported item type (%d)\n", type);
	return PAM_BAD_ITEM;
    }

    return PAM_SUCCESS;
}

int pam_set_item(pam_handle_t *pamh, int type, const void *item)
{
    switch (type) {
    case PAM_USER:
	user = malloc(strlen(item)+1);
	strcpy(user, item);
	break;

    default:
	fprintf(stderr, "unsupported item type (%d)\n", type);
	return PAM_BAD_ITEM;
    }

    return PAM_SUCCESS;
}

int pam_get_data(const pam_handle_t *pamh, const char *id, const void **dp)
{
    return PAM_NO_MODULE_DATA;
}

int pam_set_data(pam_handle_t *pamh, const char *id, void *dp,
		 void (*cleanup)(pam_handle_t *pamh, void *data,
				 int error_status))
{
    return PAM_SUCCESS;
}

static int save_fp(const char *filename, pamc_bp_t bp)
{
    DDT *ddt;
    DIT *dit = NULL;
    unsigned char header[3];
    int fd, quality, i, len, retval = PAM_ABORT;

    D(("called"));

    {
	double length;
	BYTE dark_field;

	DBI_InitDriver(&length, &dark_field);
	DBI_DLLInit(length, dark_field);
/*    DBI_DLLInit(0.4528, FALSE);   initialize the recognition library */
    }

    ddt = malloc(DBI_sizeofDDT());
    if (ddt == NULL) {
	fprintf(stderr, "no memory for DDT\n");
	return PAM_BUF_ERR;
    }

    quality = make_ddt(PAM_BP_DATA(bp), ddt);
    switch (quality & 0xFF00) {
    case DBI_PRINT_NORMAL:
    case DBI_PRINT_DRY:
	break;
    default:
	fprintf(stderr, "not likely to get a good match from this (%x)\n",
		quality);
	break;
    }

    fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR);
    if (fd == -1) {
	fprintf(stderr, "unable to open filename [%s]\n", filename);
	goto clean_up;
    }

    len = DBI_sizeofDIT();
    dit = malloc(len);
    DBI_Compress_DIT(ddt, dit);

    header[0] = 1;
    header[1] = len >> 8;
    header[2] = len & 0xFF;

    write(fd, header, 3);
    i = 0;
    while (i<len) {
	int raw = write(fd, i + (char *) dit, len-i);

	if (raw <= 0) {
	    fprintf(stderr, "failed to write full DIT to file\n");
	    goto clean_up;
	}
	i += raw;
    }

    retval = PAM_SUCCESS;

 clean_up:

    close(fd);

    if (dit) {
	memset(dit, 0, DBI_sizeofDDT());
	free(dit);
	dit = NULL;
    }

    memset(ddt, 0, DBI_sizeofDDT());
    free(ddt);
    ddt = NULL;
    
    return retval;
}

#endif /* defined(PROGRAM) */

static int do_conversation(pam_handle_t *pamh, pamc_bp_t *bp_p)
{
    int retval;
    const struct pam_conv *cs;
    struct pam_message message;
    const struct pam_message *m[1];
    struct pam_response *r = NULL;

    if (pam_get_item(pamh, PAM_CONV, (const void **)&cs) != PAM_SUCCESS) {
	D(("failed to get conversation function"));
	return PAM_CONV_ERR;
    }

    m[0] = &message;
    message.msg_style = PAM_BINARY_PROMPT;
    message.msg = (const char *) *bp_p;

    D(("here is the conversation piece: [%s]", PAM_BP_DATA(*bp_p)));
    
    retval = cs->conv(1, m, &r, cs->appdata_ptr);
    PAM_BP_RENEW(bp_p, 0, 0);
    if ((retval == PAM_SUCCESS) && r) {
	*bp_p = (pamc_bp_t) r[0].resp;
	r[0].resp = NULL;
    } else {
	D(("we are in trouble"));
	if (r == NULL) {
	    retval = PAM_CONV_ERR;
	}
    }

    if (r) {
	_pam_drop_reply(r, 1);
    }

#ifdef PROGRAM
    {
	int t=0;
	int i;
	for (i=0; i<240*240; ++i) {
	    t = t*13 + PAM_BP_DATA(*bp_p)[i];
	}
	D(("checksum: %x\n", t));
    }
#endif /* PROGRAM */

    D(("completed %d", retval));
    return retval;
}

int pam_sm_authenticate(pam_handle_t *pamh, int flags,
			int argc, const char **argv)
{
    const char *user = NULL;
    pamc_bp_t bp = NULL;
    int retval, length;

    /* construct the candidate prompt */
    if ((pam_get_item(pamh, PAM_USER, (const void **) &user) != PAM_SUCCESS)
	|| (user == NULL)) {
	D(("no pre-existing username defined"));
	user = "";
	length = 0;
    } else {
	length = strlen(user);
    }

    PAM_BP_RENEW(&bp, PAM_BPC_SELECT, sizeof(AGENT_PREFIX)+length);
    PAM_BP_FILL(bp, 0, sizeof(AGENT_PREFIX)-1, AGENT_PREFIX);
    /* the next choice should also be selectable from an argument */
    PAM_BP_FILL(bp, sizeof(AGENT_PREFIX)-1, 1, *user ? "1":"0");
    PAM_BP_FILL(bp, sizeof(AGENT_PREFIX), length, user);

    retval = do_conversation(pamh, &bp);
    if (retval != PAM_SUCCESS) {
	goto log_retval;
    }

    if (PAM_BP_CONTROL(bp) != PAM_BPC_DONE) {
	retval = PAM_AUTH_ERR;
	goto log_retval;
    }

    if (PAM_BP_LENGTH(bp) < 240*240) {
	retval = PAM_AUTH_ERR;
	D(("bad fp (%u %u)", PAM_BP_CONTROL(bp), PAM_BP_LENGTH(bp)));
	goto log_retval;
    }

    D(("verify that a user name was supplied"));

    if (*user) {
	if (strcmp(user, (240*240) + PAM_BP_DATA(bp))) {
	    D(("agent has mischievously changed user name"));
	    retval = PAM_AUTH_ERR;
	    goto log_retval;
	}
    } else if (*((240*240) + PAM_BP_DATA(bp))) {
	retval = pam_set_item(pamh, PAM_USER, (240*240) + PAM_BP_DATA(bp));
	if (retval != PAM_SUCCESS) {
	    D(("trouble setting username from binary prompt data"));
	    goto log_retval;
	}
    } else {
	D(("no username supplied"));
    }

    D(("got a fingerprint - need to ensure its acceptable"));

#ifdef PROGRAM
    if (filename) {
	retval = save_fp(filename, bp);
    } else
#endif /* PROGRAM */
    retval = authenticate_fp(pamh, bp);
    
 log_retval:
    PAM_BP_RENEW(&bp, 0, 0);
    pam_set_data(pamh, RETVAL_FP240, (void *) retval, NULL);

    return retval;
}

int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    int saved_return, retval;

    retval = pam_get_data(pamh, RETVAL_FP240, (const void **) &saved_return);
    if (retval != PAM_SUCCESS) {
	D(("authentication step did not register a retval"));
	return PAM_CRED_UNAVAIL;
    }

    switch (flags & ~(PAM_ESTABLISH_CRED|PAM_DELETE_CRED
		      |PAM_REINITIALIZE_CRED|PAM_REFRESH_CRED)) {

    case PAM_ESTABLISH_CRED:
    case PAM_DELETE_CRED:
	D(("returning %d", saved_return));
	return saved_return;

    default:
	D(("not supporting setcred %d flags", flags));
	return PAM_IGNORE;
    }
}

#if defined(PROGRAM)

/*
 * Compiling this module with PROGRAM defined, will turn this module
 * source into the source for a program that will invoke the agent
 * directly.
 *
 *    usage: ./fp240 [user] [filename]
 *
 * Running it in this way will append fingerprints to the user's
 * personal cache of official prints.
 */

int main(int argc, char **argv)
{
    int retval, exitval;

    if (argc > 1) {
	user = argv[1];
    }

    if (argc > 2) {
	filename = argv[2];
    }

    pc.appdata_ptr = pamc_start();
    if (pc.appdata_ptr == NULL) {
	fprintf(stderr, "client library was not initialized\n");
	exit(1);
    }

    exitval = pam_sm_authenticate(NULL, 0, 0, NULL);

    retval = pamc_end((pamc_handle_t *) &(pc.appdata_ptr));
    if (retval != PAM_BPC_TRUE) {
	fprintf(stderr, "client library was not happy to shut down (%d)\n",
		retval);
    }

    fprintf(stderr, "exitval = %d\n", exitval);
    exit(exitval);
}

#endif /* defined(PROGRAM) */

#ifndef HAVE___BZERO
/*
 * the biomouse library seems to need this on RH 5.2 but not RH 6.0
 * looks like a compiler issue
 */
void __bzero(void *x, int n)
{
    memset(x, 0, n);
}
#endif /* HAVE___BZERO */
