/*
 * Copyright (C) 1998 by the FundsXpress, INC.
 * 
 * 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 FundsXpress. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  FundsXpress makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "k5-int.h"
#include "enc_provider.h"

/* This random number generator is a feedback generator based on a
   block cipher.  It uses DES by default, since it guaranteed to be
   present in the system, but can be changed.  As new seed data comes
   in, the old state is folded with the new seed into new state.  Each
   time random bytes are requested, the seed is used as a key and
   cblock, and the encryption is used as the output.  The output is
   fed back as new seed data, as described above. */

/* this can be replaced with another encryption provider, since
   everything below uses it abstractly */

static const struct krb5_enc_provider *const enc = &krb5_enc_des;

/* XXX state.  Should it be in krb5_context? */

static int inited = 0;
static size_t blocksize, keybytes, keylength;
static int random_count;
/* keybytes | state-block | encblock | key | new-keybytes | new-state-block */
static unsigned char *random_state; 
#define STATE (random_state)
#define STATEKEY (STATE)
#define STATEBLOCK (STATEKEY+keybytes)
#define STATESIZE (keybytes+blocksize)
#define OUTPUT (STATE)
#define OUTPUTSIZE (STATESIZE+blocksize)
#define RANDBLOCK (STATEBLOCK+blocksize)
#define KEYCONTENTS (RANDBLOCK+blocksize)
#define NEWSTATE (KEYCONTENTS+keylength)
#define ALLSTATESIZE (keybytes+blocksize*2+keylength+keybytes+blocksize)

KRB5_DLLIMP krb5_error_code KRB5_CALLCONV
krb5_c_random_seed(krb5_context context, krb5_data *data)
{
    unsigned char *fold_input;

    if (inited == 0) {
	/* this does a bunch of malloc'ing up front, so that
	   generating random keys doesn't have to malloc, so it can't
	   fail.  seeding still malloc's, but that's less common. */

	enc->block_size(&blocksize);
	enc->keysize(&keybytes, &keylength);
	if ((random_state = (unsigned char *) malloc(ALLSTATESIZE)) == NULL)
	    return(ENOMEM);
	random_count = 0;
	inited = 1;

	krb5_nfold(data->length*8, data->data, STATESIZE*8, STATE);

	return(0);
    }

    if ((fold_input =
	 (unsigned char *) malloc(data->length+STATESIZE)) == NULL)
	return(ENOMEM);

    memcpy(fold_input, data->data, data->length);
    memcpy(fold_input+data->length, STATE, STATESIZE);

    krb5_nfold((data->length+STATESIZE)*8, fold_input,
	       STATESIZE*8, STATE);
    free(fold_input);
    return(0);
}

KRB5_DLLIMP krb5_error_code KRB5_CALLCONV
krb5_c_random_make_octets(krb5_context context, krb5_data *data)
{
    krb5_error_code ret;
    krb5_data data1, data2;
    krb5_keyblock key;
    int bytes;

    if (inited == 0) {
	/* i need some entropy.  I'd use the current time and pid, but
	   that could cause portability problems. */
	abort();
    }

    bytes = 0;

    while (bytes < data->length) {
	if (random_count == 0) {
	    /* set up random krb5_data, and key to be filled in */
	    data1.length = keybytes;
	    data1.data = STATEKEY;
	    key.length = keylength;
	    key.contents = KEYCONTENTS;

	    /* fill it in */
	    if ((ret = ((*(enc->make_key))(&data1, &key))))
		return(ret);

	    /* encrypt the block */
	    data1.length = blocksize;
	    data1.data = STATEBLOCK;
	    data2.length = blocksize;
	    data2.data = RANDBLOCK;
	    if ((ret = ((*(enc->encrypt))(&key, NULL, &data1, &data2))))
		return(ret);

	    /* fold the new output back into the state */

	    krb5_nfold(OUTPUTSIZE*8, OUTPUT, STATESIZE*8, NEWSTATE);
	    memcpy(STATE, NEWSTATE, STATESIZE);

	    random_count = blocksize;
	}

	if ((data->length - bytes) <= random_count) {
	    memcpy(data->data + bytes, RANDBLOCK+(blocksize-random_count),
		   data->length - bytes);
	    random_count -= (data->length - bytes);
	    break;
	}

	memcpy(data->data + bytes, RANDBLOCK+(blocksize - random_count),
	       random_count);

	bytes += random_count;
	random_count = 0;
    }

    return(0);
}

void prng_cleanup (void)
{
	free (random_state);
	inited = 0;
}
