/*
 * Copyright 1995, 2003 by the Massachusetts Institute of Technology.  All
 * Rights Reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 *
 */

/*
 * This file contains routines for establishing, verifying, and any other
 * necessary functions, for utilizing the pre-authentication field of the 
 * kerberos kdc request, with various hardware/software verification devices.
 */

#include "k5-int.h"
#include "pkinit_client.h"
#include "pkinit_cert_store.h"

typedef krb5_error_code (*pa_function)(krb5_context,
				       krb5_kdc_req *request,
				       krb5_pa_data *in_padata,
				       krb5_pa_data **out_padata,
				       krb5_data *salt, krb5_data *s2kparams,
				       krb5_enctype *etype,
				       krb5_keyblock *as_key,
				       krb5_prompter_fct prompter_fct,
				       void *prompter_data,
				       krb5_gic_get_as_key_fct gak_fct,
				       void *gak_data,
				       krb5_boolean *terminate);    // no more PA possible
				 
typedef struct _pa_types_t {
    krb5_preauthtype type;
    pa_function fct;
    int flags;
} pa_types_t;

#define PA_REAL 0x0001
#define PA_INFO 0x0002

static
krb5_error_code pa_salt(krb5_context context,
			krb5_kdc_req *request,
			krb5_pa_data *in_padata,
			krb5_pa_data **out_padata,
			krb5_data *salt, krb5_data *s2kparams,
			krb5_enctype *etype,
			krb5_keyblock *as_key,
			krb5_prompter_fct prompter, void *prompter_data,
			krb5_gic_get_as_key_fct gak_fct, void *gak_data,
			krb5_boolean *terminate)
{
    krb5_data tmp;

    tmp.data = in_padata->contents;
    tmp.length = in_padata->length;
    krb5_free_data_contents(context, salt);
    krb5int_copy_data_contents(context, &tmp, salt);
    

    if (in_padata->pa_type == KRB5_PADATA_AFS3_SALT)
	salt->length = SALT_TYPE_AFS_LENGTH;

    return(0);
}

static
krb5_error_code pa_enc_timestamp(krb5_context context,
				 krb5_kdc_req *request,
				 krb5_pa_data *in_padata,
				 krb5_pa_data **out_padata,
				 krb5_data *salt,
				 krb5_data *s2kparams,
				 krb5_enctype *etype,
				 krb5_keyblock *as_key,
				 krb5_prompter_fct prompter,
				 void *prompter_data,
				 krb5_gic_get_as_key_fct gak_fct,
				 void *gak_data, 
				 krb5_boolean *terminate)
{
    krb5_error_code ret;
    krb5_pa_enc_ts pa_enc;
    krb5_data *tmp;
    krb5_enc_data enc_data;
    krb5_pa_data *pa;
   
    if (as_key->length == 0) {
#ifdef DEBUG
	fprintf (stderr, "%s:%d: salt len=%d", __FILE__, __LINE__,
		 salt->length);
	if (salt->length > 0)
	    fprintf (stderr, " '%*s'", salt->length, salt->data);
	fprintf (stderr, "; *etype=%d request->ktype[0]=%d\n",
		 *etype, request->ktype[0]);
#endif
       if ((ret = ((*gak_fct)(context, request->client,
			      *etype ? *etype : request->ktype[0],
			      prompter, prompter_data,
			      salt, s2kparams, as_key, gak_data))))
           return(ret);
    }

    /* now get the time of day, and encrypt it accordingly */

    if ((ret = krb5_us_timeofday(context, &pa_enc.patimestamp, &pa_enc.pausec)))
	return(ret);

    if ((ret = encode_krb5_pa_enc_ts(&pa_enc, &tmp)))
	return(ret);

#ifdef DEBUG
    fprintf (stderr, "key type %d bytes %02x %02x ...\n",
	     as_key->enctype,
	     as_key->contents[0], as_key->contents[1]);
#endif
    ret = krb5_encrypt_helper(context, as_key,
			      KRB5_KEYUSAGE_AS_REQ_PA_ENC_TS,
			      tmp, &enc_data);
#ifdef DEBUG
    fprintf (stderr, "enc data { type=%d kvno=%d data=%02x %02x ... }\n",
	     enc_data.enctype, enc_data.kvno,
	     0xff & enc_data.ciphertext.data[0],
	     0xff & enc_data.ciphertext.data[1]);
#endif

    krb5_free_data(context, tmp);

    if (ret) {
	krb5_xfree(enc_data.ciphertext.data);
	return(ret);
    }

    ret = encode_krb5_enc_data(&enc_data, &tmp);

    krb5_xfree(enc_data.ciphertext.data);

    if (ret)
	return(ret);

    if ((pa = (krb5_pa_data *) malloc(sizeof(krb5_pa_data))) == NULL) {
	krb5_free_data(context, tmp);
	return(ENOMEM);
    }

    pa->magic = KV5M_PA_DATA;
    pa->pa_type = KRB5_PADATA_ENC_TIMESTAMP;
    pa->length = tmp->length;
    pa->contents = (krb5_octet *) tmp->data;

    *out_padata = pa;

    krb5_xfree(tmp);

    return(0);
}

static 
char *sam_challenge_banner(krb5_int32 sam_type)
{
    char *label;

    switch (sam_type) {
    case PA_SAM_TYPE_ENIGMA:	/* Enigma Logic */
	label = "Challenge for Enigma Logic mechanism";
	break;
    case PA_SAM_TYPE_DIGI_PATH: /*  Digital Pathways */
    case PA_SAM_TYPE_DIGI_PATH_HEX: /*  Digital Pathways */
	label = "Challenge for Digital Pathways mechanism";
	break;
    case PA_SAM_TYPE_ACTIVCARD_DEC: /*  Digital Pathways */
    case PA_SAM_TYPE_ACTIVCARD_HEX: /*  Digital Pathways */
	label = "Challenge for Activcard mechanism";
	break;
    case PA_SAM_TYPE_SKEY_K0:	/*  S/key where  KDC has key 0 */
	label = "Challenge for Enhanced S/Key mechanism";
	break;
    case PA_SAM_TYPE_SKEY:	/*  Traditional S/Key */
	label = "Challenge for Traditional S/Key mechanism";
	break;
    case PA_SAM_TYPE_SECURID:	/*  Security Dynamics */
	label = "Challenge for Security Dynamics mechanism";
	break;
    case PA_SAM_TYPE_SECURID_PREDICT:	/* predictive Security Dynamics */
	label = "Challenge for Security Dynamics mechanism";
	break;
    default:
	label = "Challenge from authentication server";
	break;
    }

    return(label);
}

/* this macro expands to the int,ptr necessary for "%.*s" in an sprintf */

#define SAMDATA(kdata, str, maxsize) \
	(int)((kdata.length)? \
	      ((((kdata.length)<=(maxsize))?(kdata.length):strlen(str))): \
	      strlen(str)), \
	(kdata.length)? \
	((((kdata.length)<=(maxsize))?(kdata.data):(str))):(str)

/* XXX Danger! This code is not in sync with the kerberos-password-02
   draft.  This draft cannot be implemented as written.  This code is
   compatible with earlier versions of mit krb5 and cygnus kerbnet. */

static
krb5_error_code pa_sam(krb5_context context,
		       krb5_kdc_req *request,
		       krb5_pa_data *in_padata,
		       krb5_pa_data **out_padata,
		       krb5_data *salt,
		       krb5_data *s2kparams,
		       krb5_enctype *etype,
		       krb5_keyblock *as_key,
		       krb5_prompter_fct prompter,
		       void *prompter_data,
		       krb5_gic_get_as_key_fct gak_fct,
		       void *gak_data, 
		       krb5_boolean *terminate)
{
    krb5_error_code		ret;
    krb5_data			tmpsam;
    char			name[100], banner[100];
    char			prompt[100], response[100];
    krb5_data			response_data;
    krb5_prompt			kprompt;
    krb5_prompt_type		prompt_type;
    krb5_data			defsalt;
    krb5_sam_challenge		*sam_challenge = 0;
    krb5_sam_response		sam_response;
    /* these two get encrypted and stuffed in to sam_response */
    krb5_enc_sam_response_enc	enc_sam_response_enc;
    krb5_data *			scratch;
    krb5_pa_data *		pa;

    if (prompter == NULL)
	return EIO;

    tmpsam.length = in_padata->length;
    tmpsam.data = (char *) in_padata->contents;
    if ((ret = decode_krb5_sam_challenge(&tmpsam, &sam_challenge)))
	return(ret);

    if (sam_challenge->sam_flags & KRB5_SAM_MUST_PK_ENCRYPT_SAD) {
	krb5_xfree(sam_challenge);
	return(KRB5_SAM_UNSUPPORTED);
    }

    /* If we need the password from the user (USE_SAD_AS_KEY not set),	*/
    /* then get it here.  Exception for "old" KDCs with CryptoCard 	*/
    /* support which uses the USE_SAD_AS_KEY flag, but still needs pwd	*/ 

    if (!(sam_challenge->sam_flags & KRB5_SAM_USE_SAD_AS_KEY) ||
	(sam_challenge->sam_type == PA_SAM_TYPE_CRYPTOCARD)) {

	/* etype has either been set by caller or by KRB5_PADATA_ETYPE_INFO */
	/* message from the KDC.  If it is not set, pick an enctype that we */
	/* think the KDC will have for us.				    */

	if (etype && *etype == 0)
	   *etype = ENCTYPE_DES_CBC_CRC;

	if ((ret = (gak_fct)(context, request->client, *etype, prompter,
			prompter_data, salt, s2kparams, as_key, gak_data)))
	   return(ret);
    }
    sprintf(name, "%.*s",
	    SAMDATA(sam_challenge->sam_type_name, "SAM Authentication",
		    sizeof(name) - 1));

    sprintf(banner, "%.*s",
	    SAMDATA(sam_challenge->sam_challenge_label,
		    sam_challenge_banner(sam_challenge->sam_type),
		    sizeof(banner)-1));

    /* sprintf(prompt, "Challenge is [%s], %s: ", challenge, prompt); */
    sprintf(prompt, "%s%.*s%s%.*s",
	    sam_challenge->sam_challenge.length?"Challenge is [":"",
	    SAMDATA(sam_challenge->sam_challenge, "", 20),
	    sam_challenge->sam_challenge.length?"], ":"",
	    SAMDATA(sam_challenge->sam_response_prompt, "passcode", 55));

    response_data.data = response;
    response_data.length = sizeof(response);

    kprompt.prompt = prompt;
    kprompt.hidden = 1;
    kprompt.reply = &response_data;
    prompt_type = KRB5_PROMPT_TYPE_PREAUTH;

    /* PROMPTER_INVOCATION */
    krb5int_set_prompt_types(context, &prompt_type);
    if ((ret = ((*prompter)(context, prompter_data, name,
			   banner, 1, &kprompt)))) {
	krb5_xfree(sam_challenge);
	krb5int_set_prompt_types(context, 0);
	return(ret);
    }
    krb5int_set_prompt_types(context, 0);

    enc_sam_response_enc.sam_nonce = sam_challenge->sam_nonce;
    if (sam_challenge->sam_nonce == 0) {
	if ((ret = krb5_us_timeofday(context, 
				&enc_sam_response_enc.sam_timestamp,
				&enc_sam_response_enc.sam_usec))) {
		krb5_xfree(sam_challenge);
		return(ret);
	}

	sam_response.sam_patimestamp = enc_sam_response_enc.sam_timestamp;
    }

    /* XXX What if more than one flag is set?  */
    if (sam_challenge->sam_flags & KRB5_SAM_SEND_ENCRYPTED_SAD) {

	/* Most of this should be taken care of before we get here.  We	*/
	/* will need the user's password and as_key to encrypt the SAD	*/
	/* and we want to preserve ordering of user prompts (first	*/
	/* password, then SAM data) so that user's won't be confused.	*/

	if (as_key->length) {
	    krb5_free_keyblock_contents(context, as_key);
	    as_key->length = 0;
	}

	/* generate a salt using the requested principal */

	if ((salt->length == -1 || salt->length == SALT_TYPE_AFS_LENGTH) && (salt->data == NULL)) {
	    if ((ret = krb5_principal2salt(context, request->client,
					  &defsalt))) {
		krb5_xfree(sam_challenge);
		return(ret);
	    }

	    salt = &defsalt;
	} else {
	    defsalt.length = 0;
	}

	/* generate a key using the supplied password */

	ret = krb5_c_string_to_key(context, ENCTYPE_DES_CBC_MD5,
				   (krb5_data *)gak_data, salt, as_key);

	if (defsalt.length)
	    krb5_xfree(defsalt.data);

	if (ret) {
	    krb5_xfree(sam_challenge);
	    return(ret);
	}

	/* encrypt the passcode with the key from above */

	enc_sam_response_enc.sam_sad = response_data;
    } else if (sam_challenge->sam_flags & KRB5_SAM_USE_SAD_AS_KEY) {

	/* process the key as password */

	if (as_key->length) {
	    krb5_free_keyblock_contents(context, as_key);
	    as_key->length = 0;
	}

#if 0
	if ((salt->length == SALT_TYPE_AFS_LENGTH) && (salt->data == NULL)) {
	    if (ret = krb5_principal2salt(context, request->client,
					  &defsalt)) {
		krb5_xfree(sam_challenge);
		return(ret);
	    }

	    salt = &defsalt;
	} else {
	    defsalt.length = 0;
	}
#else
	defsalt.length = 0;
	salt = NULL;
#endif
	    
	/* XXX As of the passwords-04 draft, no enctype is specified,
	   the server uses ENCTYPE_DES_CBC_MD5. In the future the
	   server should send a PA-SAM-ETYPE-INFO containing the enctype. */

	ret = krb5_c_string_to_key(context, ENCTYPE_DES_CBC_MD5,
				   &response_data, salt, as_key);

	if (defsalt.length)
	    krb5_xfree(defsalt.data);

	if (ret) {
	    krb5_xfree(sam_challenge);
	    return(ret);
	}

	enc_sam_response_enc.sam_sad.length = 0;
    } else {
	/* Eventually, combine SAD with long-term key to get
	   encryption key.  */
	return KRB5_PREAUTH_BAD_TYPE;
    }

    /* copy things from the challenge */
    sam_response.sam_nonce = sam_challenge->sam_nonce;
    sam_response.sam_flags = sam_challenge->sam_flags;
    sam_response.sam_track_id = sam_challenge->sam_track_id;
    sam_response.sam_type = sam_challenge->sam_type;
    sam_response.magic = KV5M_SAM_RESPONSE;

    krb5_xfree(sam_challenge);

    /* encode the encoded part of the response */
    if ((ret = encode_krb5_enc_sam_response_enc(&enc_sam_response_enc,
						&scratch)))
	return(ret);

    ret = krb5_encrypt_data(context, as_key, 0, scratch,
			    &sam_response.sam_enc_nonce_or_ts);

    krb5_free_data(context, scratch);

    if (ret)
	return(ret);

    /* sam_enc_key is reserved for future use */
    sam_response.sam_enc_key.ciphertext.length = 0;

    if ((pa = malloc(sizeof(krb5_pa_data))) == NULL)
	return(ENOMEM);

    if ((ret = encode_krb5_sam_response(&sam_response, &scratch))) {
	free(pa);
	return(ret);
    }

    pa->magic = KV5M_PA_DATA;
    pa->pa_type = KRB5_PADATA_SAM_RESPONSE;
    pa->length = scratch->length;
    pa->contents = (krb5_octet *) scratch->data;

    *out_padata = pa;

    return(0);
}

/* 
 * PKINIT. One function to generate AS-REQ, one to parse AS-REP
 */
#define  PKINIT_DEBUG    0
#if     PKINIT_DEBUG
#define kdcPkinitDebug(args...)       printf(args)
#else
#define kdcPkinitDebug(args...)
#endif

static krb5_error_code pa_pkinit_gen_req(
    krb5_context context,
    krb5_kdc_req *request,
    krb5_pa_data *in_padata,
    krb5_pa_data **out_padata,
    krb5_data *salt, 
    krb5_data *s2kparams,
    krb5_enctype *etype,
    krb5_keyblock *as_key,
    krb5_prompter_fct prompter, 
    void *prompter_data,
    krb5_gic_get_as_key_fct gak_fct, 
    void *gak_data,
    krb5_boolean *terminate)
{
    krb5_error_code	    krtn;
    krb5_data		    out_data = {0, 0, NULL};
    krb5_timestamp	    ctime = 0;
    krb5_ui_4		    cusec = 0;
    krb5_ui_4		    nonce = 0;
    krb5_checksum	    cksum;
    pkinit_signing_cert_t   client_cert;
    krb5_data		    *der_req = NULL;
    char		    *princ_name = NULL;
    
    kdcPkinitDebug("pa_pkinit_gen_req\n");

    /* If we don't have a client cert, we're done */
    if(request->client == NULL) {
	kdcPkinitDebug("No request->client; aborting PKINIT\n");
	return KRB5KDC_ERR_PREAUTH_FAILED;
    }
    krtn = krb5_unparse_name(context, request->client, &princ_name);
    if(krtn) {
	return krtn;
    }
    krtn = pkinit_get_client_cert(princ_name, &client_cert);
    free(princ_name);
    if(krtn) {
	kdcPkinitDebug("No client cert; aborting PKINIT\n");
	return krtn;
    }
    
    /* checksum of the encoded KDC-REQ-BODY */
    krtn = encode_krb5_kdc_req_body(request, &der_req);
    if(krtn) {
	kdcPkinitDebug("encode_krb5_kdc_req_body returned %d\n", (int)krtn);
	goto cleanup;
    }
    krtn = krb5_c_make_checksum(context, CKSUMTYPE_NIST_SHA, NULL, 0, der_req, &cksum);
    if(krtn) {
	goto cleanup;
    }

    krtn = krb5_us_timeofday(context, &ctime, &cusec);
    if(krtn) {
	goto cleanup;
    }
    /* FIXME - how to store this nonce for later comparison? */
    pkinit_rand(&nonce, sizeof(nonce));

    krtn = pkinit_as_req_create(ctime, cusec, nonce, &cksum,
	client_cert, NULL, &out_data);
    if(krtn) {
	kdcPkinitDebug("error %d on pkinit_as_req_create; aborting PKINIT\n", (int)krtn);
	goto cleanup;
    }
    *out_padata = (krb5_pa_data *)malloc(sizeof(krb5_pa_data));
    if(*out_padata == NULL) {
	krtn = ENOMEM;
	free(out_data.data);
	goto cleanup;
    }
    (*out_padata)->magic = KV5M_PA_DATA;
    (*out_padata)->pa_type = KRB5_PADATA_PK_AS_REQ;
    (*out_padata)->length = out_data.length;
    (*out_padata)->contents = out_data.data;
    krtn = 0;
cleanup:
    if(client_cert) {
	pkinit_release_cert(client_cert);
    }
    if(cksum.contents) {
	free(cksum.contents);
    }
    if (der_req) {
	krb5_free_data(context, der_req);
    }
    return krtn;

}

static krb5_error_code pa_pkinit_parse_rep(
    krb5_context context,
    krb5_kdc_req *request,
    krb5_pa_data *in_padata,
    krb5_pa_data **out_padata,
    krb5_data *salt, 
    krb5_data *s2kparams,
    krb5_enctype *etype,
    krb5_keyblock *as_key,
    krb5_prompter_fct prompter, 
    void *prompter_data,
    krb5_gic_get_as_key_fct gak_fct, 
    void *gak_data,
    krb5_boolean *terminate)
{
    krb5_boolean	is_signed;
    krb5_boolean	is_encrypted;
    krb5_ui_4		nonce;	
    pki_cert_sig_status sig_status = (pki_cert_sig_status)-999;
    krb5_error_code     krtn;
    krb5_data		asRep;
    krb5_keyblock       local_key;
    pkinit_signing_cert_t   client_cert;
    char		*princ_name = NULL;
    
    
    /*
     * One way or the other - success or failure - no other PA systems can
     * work if the server sent us a PKINIT reply, since only we know how to 
     * decrypt the key.
     */
    *terminate = TRUE;
    *out_padata = NULL;
    kdcPkinitDebug("pa_pkinit_parse_rep\n");
    if((in_padata == NULL) || (in_padata->length== 0)) {
	kdcPkinitDebug("pa_pkinit_parse_rep: no in_padata\n");
	return KRB5KDC_ERR_PREAUTH_FAILED;
    }

    kdcPkinitDebug("pa_pkinit_gen_req\n");

    /* If we don't have a client cert, we're done */
    if(request->client == NULL) {
	kdcPkinitDebug("No request->client; aborting PKINIT\n");
	return KRB5KDC_ERR_PREAUTH_FAILED;
    }
    krtn = krb5_unparse_name(context, request->client, &princ_name);
    if(krtn) {
	return krtn;
    }
    krtn = pkinit_get_client_cert(princ_name, &client_cert);
    free(princ_name);
    if(krtn) {
	kdcPkinitDebug("No client cert; aborting PKINIT\n");
	return krtn;
    }
    
    memset(&local_key, 0, sizeof(local_key));
    asRep.data = in_padata->contents;
    asRep.length = in_padata->length;
    krtn = pkinit_as_rep_parse(&asRep, client_cert, &local_key, &nonce, &sig_status,
	&is_signed, &is_encrypted,
	/* don't care about returned certs - do we? */
	NULL, NULL, NULL);
    if(krtn) {
	kdcPkinitDebug("pkinit_as_rep_parse returned %d\n", (int)krtn);
	return krtn;
    }
    if(!is_encrypted || !is_signed) {
	kdcPkinitDebug("pa_pkinit_parse_rep: not signed and encrypted!\n");
	krtn = KRB5KDC_ERR_PREAUTH_FAILED;
	goto error_out;
    }
    switch(sig_status) {
	case pki_cs_good:
	    break;
	case pki_cs_unknown_root:   /* for now allow unknown roots */
	    kdcPkinitDebug("pa_pkinit_parse_rep: allowing unknown root\n");
	    break;
	default:
	    kdcPkinitDebug("pa_pkinit_parse_rep: bad cert/sig status %d\n", 
		(int)sig_status);
	    krtn = KRB5KDC_ERR_PREAUTH_FAILED;
	    goto error_out;
    }
    
    /* WE have the key; transfer to caller */
    if (as_key->length) {
	krb5_free_keyblock_contents(context, as_key);
    }
    *as_key = local_key;
    
    #if PKINIT_DEBUG
    fprintf(stderr, "pa_pkinit_parse_rep: SUCCESS\n");
    fprintf(stderr, "nonce 0x%x enctype %d keylen %d keydata %02x %02x %02x %02x...\n",
	(int)nonce, (int)as_key->enctype, (int)as_key->length,
	as_key->contents[0], as_key->contents[1], 
	as_key->contents[2], as_key->contents[3]);
    #endif

    /* FIXME - how do we verify the nonce? That's the only job left here. */

    return 0;
    
error_out:
    if (local_key.length) {
	krb5_free_keyblock_contents(context, &local_key);
    }
    return krtn;
}

static
krb5_error_code pa_sam_2(krb5_context context,
				krb5_kdc_req *request,
				krb5_pa_data *in_padata,
				krb5_pa_data **out_padata,
				krb5_data *salt,
			 krb5_data *s2kparams,
				krb5_enctype *etype,
				krb5_keyblock *as_key,
				krb5_prompter_fct prompter,
				void *prompter_data,
				krb5_gic_get_as_key_fct gak_fct,
				void *gak_data,
				krb5_boolean *terminate) {

   krb5_error_code retval;
   krb5_sam_challenge_2 *sc2 = NULL;
   krb5_sam_challenge_2_body *sc2b = NULL;
   krb5_data tmp_data;
   krb5_data response_data;
   char name[100], banner[100], prompt[100], response[100];
   krb5_prompt kprompt;
   krb5_prompt_type prompt_type;
   krb5_data defsalt;
   krb5_checksum **cksum;
   krb5_data *scratch = NULL;
   krb5_boolean valid_cksum = 0;
   krb5_enc_sam_response_enc_2 enc_sam_response_enc_2;
   krb5_sam_response_2 sr2;
   size_t ciph_len;
   krb5_pa_data *sam_padata;

   if (prompter == NULL)
	return KRB5_LIBOS_CANTREADPWD;

   tmp_data.length = in_padata->length;
   tmp_data.data = (char *)in_padata->contents;

   if ((retval = decode_krb5_sam_challenge_2(&tmp_data, &sc2)))
	return(retval);

   retval = decode_krb5_sam_challenge_2_body(&sc2->sam_challenge_2_body, &sc2b);

   if (retval)
	return(retval);

   if (!sc2->sam_cksum || ! *sc2->sam_cksum) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(KRB5_SAM_NO_CHECKSUM);
   }

   if (sc2b->sam_flags & KRB5_SAM_MUST_PK_ENCRYPT_SAD) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(KRB5_SAM_UNSUPPORTED);
   }

   if (!valid_enctype(sc2b->sam_etype)) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(KRB5_SAM_INVALID_ETYPE);
   }

   /* All of the above error checks are KDC-specific, that is, they	*/
   /* assume a failure in the KDC reply.  By returning anything other	*/
   /* than KRB5_KDC_UNREACH, KRB5_PREAUTH_FAILED,		*/
   /* KRB5_LIBOS_PWDINTR, or KRB5_REALM_CANT_RESOLVE, the client will	*/
   /* most likely go on to try the AS_REQ against master KDC		*/

   if (!(sc2b->sam_flags & KRB5_SAM_USE_SAD_AS_KEY)) {
	/* We will need the password to obtain the key used for	*/
	/* the checksum, and encryption of the sam_response.	*/
	/* Go ahead and get it now, preserving the ordering of	*/
	/* prompts for the user.				*/

	retval = (gak_fct)(context, request->client,
			sc2b->sam_etype, prompter,
			prompter_data, salt, s2kparams, as_key, gak_data);
	if (retval) {
	   krb5_free_sam_challenge_2(context, sc2);
	   krb5_free_sam_challenge_2_body(context, sc2b);
	   return(retval);
	}
   }

   sprintf(name, "%.*s",
	SAMDATA(sc2b->sam_type_name, "SAM Authentication",
	sizeof(name) - 1));

   sprintf(banner, "%.*s",
	SAMDATA(sc2b->sam_challenge_label,
	sam_challenge_banner(sc2b->sam_type),
	sizeof(banner)-1));

   sprintf(prompt, "%s%.*s%s%.*s",
	sc2b->sam_challenge.length?"Challenge is [":"",
	SAMDATA(sc2b->sam_challenge, "", 20),
	sc2b->sam_challenge.length?"], ":"",
	SAMDATA(sc2b->sam_response_prompt, "passcode", 55));

   response_data.data = response;
   response_data.length = sizeof(response);
   kprompt.prompt = prompt;
   kprompt.hidden = 1;
   kprompt.reply = &response_data;

   prompt_type = KRB5_PROMPT_TYPE_PREAUTH;
   krb5int_set_prompt_types(context, &prompt_type);

   if ((retval = ((*prompter)(context, prompter_data, name,
				banner, 1, &kprompt)))) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	krb5int_set_prompt_types(context, 0);
	return(retval);
   }

   krb5int_set_prompt_types(context, (krb5_prompt_type *)NULL);

   /* Generate salt used by string_to_key() */
   if ((salt->length == -1) && (salt->data == NULL)) {
	if ((retval = 
	     krb5_principal2salt(context, request->client, &defsalt))) {
	   krb5_free_sam_challenge_2(context, sc2);
	   krb5_free_sam_challenge_2_body(context, sc2b);
	   return(retval);
	}
	salt = &defsalt;
   } else {
	defsalt.length = 0;
   }

   /* Get encryption key to be used for checksum and sam_response */
   if (!(sc2b->sam_flags & KRB5_SAM_USE_SAD_AS_KEY)) {
	/* as_key = string_to_key(password) */

	if (as_key->length) {
	   krb5_free_keyblock_contents(context, as_key);
	   as_key->length = 0;
	}

	/* generate a key using the supplied password */
	retval = krb5_c_string_to_key(context, sc2b->sam_etype,
                                   (krb5_data *)gak_data, salt, as_key);

	if (retval) {
	   krb5_free_sam_challenge_2(context, sc2);
	   krb5_free_sam_challenge_2_body(context, sc2b);
	   if (defsalt.length) krb5_xfree(defsalt.data);
	   return(retval);
	}

	if (!(sc2b->sam_flags & KRB5_SAM_SEND_ENCRYPTED_SAD)) {
	   /* as_key = combine_key (as_key, string_to_key(SAD)) */
	   krb5_keyblock tmp_kb;

	   retval = krb5_c_string_to_key(context, sc2b->sam_etype,
				&response_data, salt, &tmp_kb);

	   if (retval) {
		krb5_free_sam_challenge_2(context, sc2);
	        krb5_free_sam_challenge_2_body(context, sc2b);
		if (defsalt.length) krb5_xfree(defsalt.data);
		return(retval);
	   }

	   /* This should be a call to the crypto library some day */
	   /* key types should already match the sam_etype */
	   retval = krb5int_c_combine_keys(context, as_key, &tmp_kb, as_key);

	   if (retval) {
		krb5_free_sam_challenge_2(context, sc2);
	        krb5_free_sam_challenge_2_body(context, sc2b);
		if (defsalt.length) krb5_xfree(defsalt.data);
		return(retval);
	   }
	   krb5_free_keyblock_contents(context, &tmp_kb);
	}

	if (defsalt.length)
	   krb5_xfree(defsalt.data);

   } else {
	/* as_key = string_to_key(SAD) */

	if (as_key->length) {
	   krb5_free_keyblock_contents(context, as_key);
	   as_key->length = 0;
	}

	/* generate a key using the supplied password */
	retval = krb5_c_string_to_key(context, sc2b->sam_etype,
				&response_data, salt, as_key);

	if (defsalt.length)
	   krb5_xfree(defsalt.data);

	if (retval) {
	   krb5_free_sam_challenge_2(context, sc2);
	   krb5_free_sam_challenge_2_body(context, sc2b);
	   return(retval);
	}
   }

   /* Now we have a key, verify the checksum on the sam_challenge */

   cksum = sc2->sam_cksum;
   
   while (*cksum) {
	/* Check this cksum */
	retval = krb5_c_verify_checksum(context, as_key,
			KRB5_KEYUSAGE_PA_SAM_CHALLENGE_CKSUM,
			&sc2->sam_challenge_2_body,
			*cksum, &valid_cksum);
	if (retval) {
	   krb5_free_data(context, scratch);
	   krb5_free_sam_challenge_2(context, sc2);
	   krb5_free_sam_challenge_2_body(context, sc2b);
	   return(retval);
	}
	if (valid_cksum)
	   break;
	cksum++;
   }

   if (!valid_cksum) {

	/* If KRB5_SAM_SEND_ENCRYPTED_SAD is set, then password is only	*/
	/* source for checksum key.  Therefore, a bad checksum means a	*/
	/* bad password.  Don't give that direct feedback to someone	*/
	/* trying to brute-force passwords.				*/

	if (!(sc2b->sam_flags & KRB5_SAM_SEND_ENCRYPTED_SAD))
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	/*
	 * Note: We return AP_ERR_BAD_INTEGRITY so upper-level applications
	 * can interpret that as "password incorrect", which is probably
	 * the best error we can return in this situation.
	 */
	return(KRB5KRB_AP_ERR_BAD_INTEGRITY);
   }
 
   /* fill in enc_sam_response_enc_2 */
   enc_sam_response_enc_2.magic = KV5M_ENC_SAM_RESPONSE_ENC_2;
   enc_sam_response_enc_2.sam_nonce = sc2b->sam_nonce;
   if (sc2b->sam_flags & KRB5_SAM_SEND_ENCRYPTED_SAD) {
	enc_sam_response_enc_2.sam_sad = response_data;
   } else {
	enc_sam_response_enc_2.sam_sad.data = NULL;
	enc_sam_response_enc_2.sam_sad.length = 0;
   }

   /* encode and encrypt enc_sam_response_enc_2 with as_key */
   retval = encode_krb5_enc_sam_response_enc_2(&enc_sam_response_enc_2,
		&scratch);
   if (retval) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(retval);
   }

   /* Fill in sam_response_2 */
   memset(&sr2, 0, sizeof(sr2));
   sr2.sam_type = sc2b->sam_type;
   sr2.sam_flags = sc2b->sam_flags;
   sr2.sam_track_id = sc2b->sam_track_id;
   sr2.sam_nonce = sc2b->sam_nonce;

   /* Now take care of sr2.sam_enc_nonce_or_sad by encrypting encoded	*/
   /* enc_sam_response_enc_2 from above */

   retval = krb5_c_encrypt_length(context, as_key->enctype, scratch->length,
				  &ciph_len);
   if (retval) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(retval);
   }
   sr2.sam_enc_nonce_or_sad.ciphertext.length = ciph_len;

   sr2.sam_enc_nonce_or_sad.ciphertext.data =
	(char *)malloc(sr2.sam_enc_nonce_or_sad.ciphertext.length);

   if (!sr2.sam_enc_nonce_or_sad.ciphertext.data) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	return(ENOMEM);
   }

   retval = krb5_c_encrypt(context, as_key, KRB5_KEYUSAGE_PA_SAM_RESPONSE,
		NULL, scratch, &sr2.sam_enc_nonce_or_sad);
   if (retval) {
	krb5_free_sam_challenge_2(context, sc2);
	krb5_free_sam_challenge_2_body(context, sc2b);
	krb5_free_data(context, scratch);
	krb5_free_data_contents(context, &sr2.sam_enc_nonce_or_sad.ciphertext);
	return(retval);
   }
   krb5_free_data(context, scratch);
   scratch = NULL;

   /* Encode the sam_response_2 */
   retval = encode_krb5_sam_response_2(&sr2, &scratch);
   krb5_free_sam_challenge_2(context, sc2);
   krb5_free_sam_challenge_2_body(context, sc2b);
   krb5_free_data_contents(context, &sr2.sam_enc_nonce_or_sad.ciphertext);

   if (retval) {
	return (retval);
   }

   /* Almost there, just need to make padata !  */
   sam_padata = malloc(sizeof(krb5_pa_data));
   if (sam_padata == NULL) {
	krb5_free_data(context, scratch);
	return(ENOMEM);
   }

   sam_padata->magic = KV5M_PA_DATA;
   sam_padata->pa_type = KRB5_PADATA_SAM_RESPONSE_2;
   sam_padata->length = scratch->length;
   sam_padata->contents = (krb5_octet *) scratch->data;

   *out_padata = sam_padata;

   return(0);
}

/* FIXME - order significant? */
static const pa_types_t pa_types[] = {
    {
	KRB5_PADATA_PW_SALT,
	pa_salt,
	PA_INFO,
    },
    {
	KRB5_PADATA_AFS3_SALT,
	pa_salt,
	PA_INFO,
    },
    {
	KRB5_PADATA_PK_AS_REQ,
	pa_pkinit_gen_req,
	PA_INFO,
    },
    {
	KRB5_PADATA_PK_AS_REP,
	pa_pkinit_parse_rep,
	PA_REAL,
    },
    {
	KRB5_PADATA_ENC_TIMESTAMP,
	pa_enc_timestamp,
	PA_REAL,
    },
    {
	KRB5_PADATA_SAM_CHALLENGE_2,
	pa_sam_2,
	PA_REAL,
    },
    {
	KRB5_PADATA_SAM_CHALLENGE,
	pa_sam,
	PA_REAL,
    },
    {
	-1,
	NULL,
	0,
    },
};

krb5_error_code
krb5_do_preauth(krb5_context context,
		krb5_kdc_req *request,
		krb5_pa_data **in_padata, krb5_pa_data ***out_padata,
		krb5_data *salt, krb5_data *s2kparams,
		krb5_enctype *etype,
		krb5_keyblock *as_key,
		krb5_prompter_fct prompter, void *prompter_data,
		krb5_gic_get_as_key_fct gak_fct, void *gak_data)
{
    int h, i, j, out_pa_list_size;
    int seen_etype_info2 = 0;
    krb5_pa_data *out_pa = NULL, **out_pa_list = NULL;
    krb5_data scratch;
    krb5_etype_info etype_info = NULL;
    krb5_error_code ret;
    static const int paorder[] = { PA_INFO, PA_REAL };
    int realdone;

    if (in_padata == NULL) {
	*out_padata = NULL;
	return(0);
    }

#ifdef DEBUG
    fprintf (stderr, "salt len=%d", salt->length);
    if (salt->length > 0)
	fprintf (stderr, " '%*s'", salt->length, salt->data);
    fprintf (stderr, "; preauth data types:");
    for (i = 0; in_padata[i]; i++) {
	fprintf (stderr, " %d", in_padata[i]->pa_type);
    }
    fprintf (stderr, "\n");
#endif

    out_pa_list = NULL;
    out_pa_list_size = 0;

    /* first do all the informational preauths, then the first real one */

    for (h=0; h<(sizeof(paorder)/sizeof(paorder[0])); h++) {
	realdone = 0;
	for (i=0; in_padata[i] && !realdone; i++) {
	    int k, l, etype_found, valid_etype_found;
	    /*
	     * This is really gross, but is necessary to prevent
	     * lossge when talking to a 1.0.x KDC, which returns an
	     * erroneous PA-PW-SALT when it returns a KRB-ERROR
	     * requiring additional preauth.
	     */
	    switch (in_padata[i]->pa_type) {
	    case KRB5_PADATA_ETYPE_INFO:
	    case KRB5_PADATA_ETYPE_INFO2:
	    {
		krb5_preauthtype pa_type = in_padata[i]->pa_type;
		if (etype_info) {
		    if (seen_etype_info2 || pa_type != KRB5_PADATA_ETYPE_INFO2)
			continue;
		    if (pa_type == KRB5_PADATA_ETYPE_INFO2) {
			krb5_free_etype_info( context, etype_info);
			etype_info = NULL;
		    }
		}

		scratch.length = in_padata[i]->length;
		scratch.data = (char *) in_padata[i]->contents;
		if (pa_type == KRB5_PADATA_ETYPE_INFO2) {
		    seen_etype_info2++;
		    ret = decode_krb5_etype_info2(&scratch, &etype_info);
		}
		else ret = decode_krb5_etype_info(&scratch, &etype_info);
		if (ret) {
		    ret = 0; /*Ignore error and etype_info element*/
		    krb5_free_etype_info( context, etype_info);
		    etype_info = NULL;
		    continue;
		}
		if (etype_info[0] == NULL) {
		    krb5_free_etype_info(context, etype_info);
		    etype_info = NULL;
		    break;
		}
		/*
		 * Select first etype in our request which is also in
		 * etype-info (preferring client request ktype order).
		 */
		for (etype_found = 0, valid_etype_found = 0, k = 0;
		     !etype_found && k < request->nktypes; k++) {
		    for (l = 0; etype_info[l]; l++) {
			if (etype_info[l]->etype == request->ktype[k]) {
			    etype_found++;
			    break;
			}
			/* check if program has support for this etype for more
			 * precise error reporting.
			 */
			if (valid_enctype(etype_info[l]->etype))
			    valid_etype_found++;
		    }
		}
		if (!etype_found) {
		  if (valid_etype_found) {
			/* supported enctype but not requested */
		    ret =  KRB5_CONFIG_ETYPE_NOSUPP;
		    goto cleanup;
		  }
		  else {
		    /* unsupported enctype */
		    ret =  KRB5_PROG_ETYPE_NOSUPP;
		    goto cleanup;
		  }

		}
		scratch.data = (char *) etype_info[l]->salt;
		scratch.length = etype_info[l]->length;
		krb5_free_data_contents(context, salt);
		if (scratch.length == KRB5_ETYPE_NO_SALT) 
		  salt->data = NULL;
		else
		    if ((ret = krb5int_copy_data_contents( context, &scratch, salt)) != 0)
		  goto cleanup;
		*etype = etype_info[l]->etype;
		krb5_free_data_contents(context, s2kparams);
		if ((ret = krb5int_copy_data_contents(context,
						      &etype_info[l]->s2kparams,
						      s2kparams)) != 0)
		  goto cleanup;
#ifdef DEBUG
		for (j = 0; etype_info[j]; j++) {
		    krb5_etype_info_entry *e = etype_info[j];
		    fprintf (stderr, "etype info %d: etype %d salt len=%d",
			     j, e->etype, e->length);
		    if (e->length > 0 && e->length != KRB5_ETYPE_NO_SALT)
			fprintf (stderr, " '%*s'", e->length, e->salt);
		    fprintf (stderr, "\n");
		}
#endif
		break;
	    }
	    case KRB5_PADATA_PW_SALT:
	    case KRB5_PADATA_AFS3_SALT:
		if (etype_info)
		    continue;
		break;
	    default:
		;
	    }
	    for (j=0; pa_types[j].type >= 0; j++) {
		if ((in_padata[i]->pa_type == pa_types[j].type) &&
		    (pa_types[j].flags & paorder[h])) {
		    krb5_boolean terminate = FALSE;
		    out_pa = NULL;

		    ret = (*pa_types[j].fct)(context, request,
						   in_padata[i], &out_pa,
						   salt, s2kparams, etype, as_key,
						   prompter, prompter_data,
						   gak_fct, gak_data, &terminate);
		    if(ret) {
			if(terminate) {
			    /* failure, and no other PA possible - we're done */
			    goto cleanup;
			}
			else {
			    /* try another */
			    continue;
			}
		    }

		    if (out_pa) {
			if (out_pa_list == NULL) {
			    if ((out_pa_list =
				 (krb5_pa_data **)
				 malloc(2*sizeof(krb5_pa_data *)))
				== NULL) {
			      ret = ENOMEM;
			      goto cleanup;
			    }
			} else {
			    if ((out_pa_list =
				 (krb5_pa_data **)
				 realloc(out_pa_list,
					 (out_pa_list_size+2)*
					 sizeof(krb5_pa_data *)))
				== NULL) {
			      /* XXX this will leak the pointers which
				   have already been allocated.  oh well. */
			      ret = ENOMEM;
			      goto cleanup;
			    }
			}
			
			out_pa_list[out_pa_list_size++] = out_pa;
		    }
		    if (paorder[h] == PA_REAL) {
			realdone = 1;
			if(terminate) {
			    break;
			}
		    }
		}   /* match of incoming pa_type with known */
	    }   /* for known pa_types */
	}   /* for each incoming pa_type */
    }	 /* INFO, then REAL */

    if (out_pa_list)
	out_pa_list[out_pa_list_size++] = NULL;

    *out_padata = out_pa_list;
    if (etype_info)
      krb5_free_etype_info(context, etype_info);
    
    return(0);
 cleanup:
    if (out_pa_list) {
      out_pa_list[out_pa_list_size++] = NULL;
      krb5_free_pa_data(context, out_pa_list);
    }
    if (etype_info)
      krb5_free_etype_info(context, etype_info);
    return (ret);
}
