/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * Mechanism Manager - centralized knowledge of mechanisms.
 *
 * The core of the mechmanager is the "mechlist" data structure. It contains
 * information about all mechanisms available from providers that have been
 * exposed to the application.
 *
 * Each element in the array represents a particular mechanism type. The
 * array is sorted by type, so that searching by mechanism can be done
 * quickly. Each element also contains the mechanism data for each slot.
 *
 * The mechlist is constructed on an as-needed basis, entries are not added
 * until the application triggers an action that requires an entry to be
 * added (or updated).
 *
 */

#include <string.h>
#include <strings.h>
#include "pkcs11Conf.h"
#include "metaGlobal.h"


/* Global data... */

#define	INITIAL_MECHLIST_SIZE	256

typedef struct mechliststruct {
	CK_MECHANISM_TYPE type;
	mechinfo_t *slots;
} mechlist_t;

static pthread_rwlock_t mechlist_lock = PTHREAD_RWLOCK_INITIALIZER;
static mechlist_t *mechlist;
static unsigned long num_mechs;
static unsigned long true_mechlist_size;


/* Prototypes... */
static CK_RV meta_mechManager_update_mech(CK_MECHANISM_TYPE, boolean_t);
static CK_RV meta_mechManager_update_slot(CK_ULONG);
static CK_RV update_slotmech(CK_MECHANISM_TYPE, CK_ULONG, unsigned long);
static CK_RV meta_mechManager_allocmechs(CK_MECHANISM_TYPE *, unsigned long,
	unsigned long *);
static boolean_t find_mech_index(CK_MECHANISM_TYPE, unsigned long *);
static int qsort_mechtypes(const void *, const void *);


/*
 * meta_mechManager_initialize
 *
 * Called from C_Initialize. Allocates and initializes storage needed
 * by the slot manager.
 */
CK_RV
meta_mechManager_initialize()
{
	/* The mechlist can dynamically grow, but let's preallocate space. */
	mechlist = calloc(INITIAL_MECHLIST_SIZE, sizeof (mechlist_t));
	if (mechlist == NULL)
		return (CKR_HOST_MEMORY);

	true_mechlist_size = INITIAL_MECHLIST_SIZE;
	num_mechs = 0;

	return (CKR_OK);
}


/*
 * meta_mechManager_finalize
 *
 * Called from C_Finalize. Deallocates any storage held by the slot manager.
 */
void
meta_mechManager_finalize()
{
	int i;

	/* No need to lock list, we assume all sessions are closed. */
	for (i = 0; i < num_mechs; i++) {
		free(mechlist[i].slots);
	}

	free(mechlist);
	mechlist = NULL;
	num_mechs = 0;
	true_mechlist_size = 0;
}


/*
 * meta_mechManager_get_mechs
 *
 * Get list of all available mechanisms.
 *
 * Follows PKCS#11 semantics, where list may be NULL to only request a
 * count of available mechanisms.
 */
CK_RV
meta_mechManager_get_mechs(CK_MECHANISM_TYPE *list, CK_ULONG *listsize)
{
	CK_RV rv = CKR_OK;
	CK_ULONG num_found = 0;
	CK_ULONG slotnum, num_slots;
	unsigned long i;

	/* get number of slots */
	num_slots = meta_slotManager_get_slotcount();

	/*
	 * Update slot info. Ignore any errors.
	 *
	 * NOTE: Due to the PKCS#11 convention of calling C_GetMechanismList
	 * twice (once to get the count, again to get the actual list), this
	 * is somewhat inefficient... However, I don't see an easy way to fix
	 * that without impacting other cases (eg, when the first call contains
	 * an "optimistic" pre-allocated buffer).
	 */
	for (slotnum = 0; slotnum < num_slots; slotnum++) {
		(void) meta_mechManager_update_slot(slotnum);
	}


	/*
	 * Count the number of mechanisms. We can't just use num_mechs,
	 * because some mechs may not currently be supported on any slot.
	 * Also, it may not be allowed based on the mechanism policy.
	 */

	(void) pthread_rwlock_rdlock(&mechlist_lock);
	for (i = 0; i < num_mechs; i++) {
		CK_ULONG j;
		boolean_t supported;

		if (pkcs11_is_dismech(METASLOT_FRAMEWORK_ID,
		    mechlist[i].type)) {
			/* skip mechs disabled by policy */
			continue;
		}

		supported = FALSE;
		for (j = 0; j < num_slots; j++) {
			if (!mechlist[i].slots[j].initialized)
				continue;

			if (mechlist[i].slots[j].supported) {
				supported = B_TRUE;
				break;
			}
		}

		if (supported) {
			num_found++;

			if (list && *listsize >= num_found) {
				list[num_found - 1] = mechlist[i].type;
			}
		}
	}
	(void) pthread_rwlock_unlock(&mechlist_lock);

	if (num_found > *listsize)
		rv = CKR_BUFFER_TOO_SMALL;

	*listsize = num_found;

	return (rv);
}


/*
 * meta_mechManager_get_slots
 *
 * Get list of all slots supporting the specified mechanism.
 *
 * The "mech_support_info" argument should have allocated enough
 * space to accomodate the list of slots that supports the
 * specified mechanism.  The "num_supporting_slots" field
 * in the "mech_support_info" structure will indicate how
 * many slots are found to support the mechanism.
 *
 * If any error occurred in getting the list, info in
 * mech_support_info argument is not updated.
 *
 */
CK_RV
meta_mechManager_get_slots(mech_support_info_t  *mech_support_info,
    boolean_t force_update)
{
	CK_RV rv;
	boolean_t found;
	CK_ULONG i, num_slots;
	unsigned long index, num_found = 0;

	rv = meta_mechManager_update_mech(mech_support_info->mech,
	    force_update);
	if (rv != CKR_OK) {
		return (rv);
	}

	(void) pthread_rwlock_rdlock(&mechlist_lock);

	found = find_mech_index(mech_support_info->mech, &index);
	if (!found) {
		goto finish;
	}

	num_slots = meta_slotManager_get_slotcount();
	for (i = 0; i < num_slots; i++) {
		if (!mechlist[index].slots[i].initialized ||
		    !mechlist[index].slots[i].supported)
			continue;

		num_found++;
		(mech_support_info->supporting_slots)[num_found - 1]
		    = &mechlist[index].slots[i];
	}

finish:
	(void) pthread_rwlock_unlock(&mechlist_lock);

	if (num_found == 0) {
		rv = CKR_MECHANISM_INVALID;
	} else {
		mech_support_info->num_supporting_slots = num_found;
	}

	return (rv);
}


/*
 * meta_mechManager_update_mech
 *
 * Updates a mechanism in the mechlist. If the mechanism is not
 * listed, all providers will be queried. If the mechanism
 * is present, but not initialized for some providers, those providers
 * will be queried. Existing entries will not be updated unless the
 * force_refresh flag is set.
 *
 * The force_refresh flag is used by C_GetMechanismInfo, to force an
 * update. Updates are not forced during the common usage by operations
 * [eg C_EncryptInit] to avoid poor performance.
 */
static CK_RV
meta_mechManager_update_mech(CK_MECHANISM_TYPE mech, boolean_t force_refresh)
{
	CK_RV rv;
	CK_ULONG slot, num_slots;
	unsigned long index = 0;
	boolean_t found;

	/* Ensure list contains the mechanism. */
	rv = meta_mechManager_allocmechs(&mech, 1, &index);
	if (rv != CKR_OK)
		return (rv);

	(void) pthread_rwlock_wrlock(&mechlist_lock);
	/*
	 * We didn't retain a lock after the first search, so it's possible
	 * that the mechlist was updated. Search again, but use the last
	 * index as a hint to quickly find the mechanism.
	 */
	found = find_mech_index(mech, &index);
	if (!found) {
		/* Shouldn't happen - entries are not removed from list. */
		rv = CKR_GENERAL_ERROR;
		goto finish;
	}

	num_slots = meta_slotManager_get_slotcount();
	for (slot = 0; slot < num_slots; slot++) {
		if (force_refresh || !mechlist[index].slots[slot].initialized) {
			rv = update_slotmech(mech, slot, index);
			if (rv != CKR_OK) {
				/* Ignore error and continue with next slot. */
				rv = CKR_OK;
			}
		}
	}

finish:
	(void) pthread_rwlock_unlock(&mechlist_lock);

	return (rv);
}


/*
 * meta_mechManager_update_slot
 *
 * Updates a slot in the mechlist. Called by C_GetMechanismList
 * [by way of meta_mechManager_get_mechs()]. Unlike
 * meta_mechManager_get_slots(), the context is always to force a refresh
 * of the mechlist.
 *
 */
static CK_RV
meta_mechManager_update_slot(CK_ULONG slotnum)
{
	unsigned long index = 0;
	CK_MECHANISM_TYPE *slot_mechlist = NULL, *tmp_slot_mechlist = NULL;
	CK_ULONG slot_mechlistsize, mechnum, tmp_mechlistsize;
	CK_RV rv;
	boolean_t found;
	CK_SLOT_ID fw_st_id, true_id;
	int i;

	fw_st_id = meta_slotManager_get_framework_table_id(slotnum);
	true_id = TRUEID(fw_st_id);

	/* First, get the count. */
	rv = FUNCLIST(fw_st_id)->C_GetMechanismList(true_id, NULL,
	    &slot_mechlistsize);
	if (rv != CKR_OK) {
		goto finish;
	}

	tmp_slot_mechlist = malloc(
	    slot_mechlistsize * sizeof (CK_MECHANISM_TYPE));
	if (tmp_slot_mechlist == NULL) {
		rv = CKR_HOST_MEMORY;
		goto finish;
	}

	/* Next, get the actual list. */
	rv = FUNCLIST(fw_st_id)->C_GetMechanismList(true_id,
	    tmp_slot_mechlist, &slot_mechlistsize);
	if (rv != CKR_OK) {
		goto finish;
	}

	/*
	 * filter the list of mechanisms returned by the underlying slot
	 * to remove any mechanisms that are explicitly disabled
	 * in the configuration file.
	 */
	slot_mechlist = malloc(slot_mechlistsize * sizeof (CK_MECHANISM_TYPE));
	if (slot_mechlist == NULL) {
		rv = CKR_HOST_MEMORY;
		goto finish;
	}

	tmp_mechlistsize = 0;
	for (i = 0; i < slot_mechlistsize; i++) {
		/* filter out the disabled mechanisms */
		if (pkcs11_is_dismech(fw_st_id, tmp_slot_mechlist[i])) {
			continue;
		}

		slot_mechlist[tmp_mechlistsize] = tmp_slot_mechlist[i];
		tmp_mechlistsize++;
	}
	slot_mechlistsize = tmp_mechlistsize;

	/* Sort the mechanisms by value. */
	qsort(slot_mechlist, slot_mechlistsize, sizeof (CK_MECHANISM_TYPE),
		qsort_mechtypes);

	/* Ensure list contains the mechanisms. */
	rv = meta_mechManager_allocmechs(slot_mechlist, slot_mechlistsize,
		&index);
	if (rv != CKR_OK)
		goto finish;

	/* Update the mechanism info. */
	(void) pthread_rwlock_wrlock(&mechlist_lock);
	for (mechnum = 0; mechnum < slot_mechlistsize; mechnum++) {
		found = find_mech_index(slot_mechlist[mechnum], &index);
		if (!found) {
			/* This shouldn't happen. */
			rv = CKR_GENERAL_ERROR;
			goto finish;
		}

		rv = update_slotmech(slot_mechlist[mechnum], slotnum, index);
		if (rv != CKR_OK) {
			/* Ignore error, make best effort to finish update. */
			rv = CKR_OK;
			continue;
		}
	}
	(void) pthread_rwlock_unlock(&mechlist_lock);

finish:
	if (slot_mechlist) {
		free(slot_mechlist);
	}

	if (tmp_slot_mechlist) {
		free(tmp_slot_mechlist);
	}

	return (rv);
}


/*
 * update_slotmech
 *
 * Updates the information for a particular mechanism for a particular slot.
 * (ie, slotlist[foo].slots[bar])
 *
 * It is assumed that the caller to this function (all of which are
 * in this file) holds the write-lock to "mechlist_lock".
 *
 */
static CK_RV
update_slotmech(CK_MECHANISM_TYPE mech, CK_ULONG slotnum,
	unsigned long index)
{
	CK_RV rv = CKR_OK;
	CK_MECHANISM_INFO info;
	CK_SLOT_ID fw_st_id, true_id;

	mechlist[index].slots[slotnum].slotnum = slotnum;
	fw_st_id = meta_slotManager_get_framework_table_id(slotnum);
	true_id = TRUEID(fw_st_id);

	/*
	 * Check if the specified mechanism is in the disabled list
	 * of the specified slot.  If so, we can immediately conclude
	 * that it is not supported by the specified slot.
	 */
	if (pkcs11_is_dismech(fw_st_id, mech)) {
		/*
		 * we mark this as initialized so that we won't try
		 * to do this check later
		 */
		mechlist[index].slots[slotnum].initialized = B_TRUE;
		mechlist[index].slots[slotnum].supported = B_FALSE;
		bzero(&mechlist[index].slots[slotnum].mechanism_info,
			sizeof (CK_MECHANISM_INFO));
		goto finish;
	}

	rv = FUNCLIST(fw_st_id)->C_GetMechanismInfo(true_id, mech, &info);
	if (rv == CKR_OK) {
		mechlist[index].slots[slotnum].initialized = B_TRUE;
		mechlist[index].slots[slotnum].supported = B_TRUE;
		mechlist[index].slots[slotnum].mechanism_info = info;
	} else {
		/* record that the mechanism isn't supported for the slot */
		mechlist[index].slots[slotnum].initialized = B_TRUE;
		mechlist[index].slots[slotnum].supported = B_FALSE;
		bzero(&mechlist[index].slots[slotnum].mechanism_info,
			sizeof (CK_MECHANISM_INFO));
	}

finish:
	return (rv);
}


/*
 * meta_mechManager_allocmechs
 *
 * Ensures that all of the specified mechanisms are present in the
 * mechlist. If a mechanism is not present, an uninitialized entry is
 * added for it.
 *
 * The returned index can be used by the caller as a hint to where the
 * first mechanism was located.
 */
static CK_RV
meta_mechManager_allocmechs(CK_MECHANISM_TYPE *new_mechs,
	unsigned long num_new_mechs, unsigned long *index_hint)
{
	CK_RV rv = CKR_OK;
	unsigned long i, index = 0;
	boolean_t found;

	/* The optimistic assumption is that the mech is already present. */
	(void) pthread_rwlock_rdlock(&mechlist_lock);
	for (i = 0; i < num_new_mechs; i++) {
		found = find_mech_index(new_mechs[i], &index);

		if (i == 0)
			*index_hint = index;

		if (!found)
			break;
	}
	(void) pthread_rwlock_unlock(&mechlist_lock);

	if (found) {
		return (CKR_OK);
	}

	/*
	 * We stopped searching when the first unknown mech was found. Now
	 * obtain a write-lock, and continue from where we left off, inserting
	 * unknown mechanisms.
	 */

	(void) pthread_rwlock_wrlock(&mechlist_lock);
	for (; i < num_new_mechs; i++) {
		found = find_mech_index(new_mechs[i], &index);

		if (!found) {
			mechinfo_t *new_mechinfos;

			new_mechinfos = calloc(meta_slotManager_get_slotcount(),
				sizeof (mechinfo_t));
			if (new_mechinfos == NULL) {
				rv = CKR_HOST_MEMORY;
				goto finish;
			}

			/*
			 * If the current storage for the mechlist is too
			 * small, allocate a new list twice as large.
			 */
			if (num_mechs == true_mechlist_size) {
				mechlist_t *newmechlist;

				newmechlist = realloc(mechlist,
					2 * true_mechlist_size *
					sizeof (mechlist_t));

				if (newmechlist == NULL) {
					rv = CKR_HOST_MEMORY;
					free(new_mechinfos);
					goto finish;
				}

				mechlist = newmechlist;
				true_mechlist_size *= 2;
			}

			/* Shift existing entries to make space. */
			(void) memmove(&mechlist[index+1], &mechlist[index],
				(num_mechs - index) * sizeof (mechlist_t));
			num_mechs++;

			mechlist[index].type = new_mechs[i];
			mechlist[index].slots = new_mechinfos;
		}
	}

finish:
	(void) pthread_rwlock_unlock(&mechlist_lock);

	return (rv);
}


/*
 * find_mech_index
 *
 * Performs a search of mechlist for the specified mechanism, and
 * returns if the mechanism was found or not. The value of the "index"
 * argument will be where the mech is (if found), or where it should
 * be (if not found).
 *
 * The current value of "index" will be used as a starting point, if the
 * caller already knows where the mechanism is likely to be.
 *
 * The caller is assumed to have a lock on the mechlist, preventing it
 * from being changed while searching (also to ensure the returned index
 * will remain valid until the list is unlocked).
 *
 * FUTURE: convert to binary search [from O(N) to a O(log(N))].
 *
 * NOTES:
 * 1) This function assumes that mechMap is a sorted list.
 */
static boolean_t
find_mech_index(CK_MECHANISM_TYPE mechanism, unsigned long *index)
{
	boolean_t found = B_FALSE;
	unsigned long i;

	for (i = 0; i < num_mechs; i++) {

		if (mechlist[i].type == mechanism) {
			found = B_TRUE;
			break;
		}

		if (mechlist[i].type > mechanism)
			break;
	}

	*index = i;

	return (found);
}

static int
qsort_mechtypes(const void *arg1, const void *arg2)
{
	CK_MECHANISM_TYPE mech1 = *((CK_MECHANISM_TYPE *)arg1);
	CK_MECHANISM_TYPE mech2 = *((CK_MECHANISM_TYPE *)arg2);

	if (mech1 > mech2)
		return (1);
	if (mech1 < mech2)
		return (-1);
	return (0);
}

/*
 * Check if the specified mechanism is supported by the specified slot.
 * The result is returned in the "supports" argument.  If the "slot_info"
 * argument is not NULL, it will be filled with information about
 * the slot.
 */
CK_RV
meta_mechManager_slot_supports_mech(CK_MECHANISM_TYPE mechanism,
    CK_ULONG slotnum, boolean_t *supports, mechinfo_t **slot_info,
    boolean_t force_update)
{

	boolean_t found;
	CK_RV rv;
	unsigned long index;

	*supports = B_FALSE;

	rv = meta_mechManager_update_mech(mechanism, force_update);
	if (rv != CKR_OK)
		return (rv);

	(void) pthread_rwlock_rdlock(&mechlist_lock);

	found = find_mech_index(mechanism, &index);
	if (!found) {
		goto finish;
	}

	if ((mechlist[index].slots[slotnum].initialized) &&
	    (mechlist[index].slots[slotnum].supported)) {
		*supports = B_TRUE;
		if (slot_info) {
			*slot_info = &(mechlist[index].slots[slotnum]);
		}
	}

finish:
	(void) pthread_rwlock_unlock(&mechlist_lock);

	return (rv);
}