/*
 * nasd_nonce_mgmt.c
 *
 * Handle checking if a nonce is fresh or resued
 *
 * Author: Marc Unangst
 */
/*
 * Copyright (c) of Carnegie Mellon University, 1996,1997,1998,1999.
 *
 * Permission to reproduce, use, and prepare derivative works of
 * this software for internal use is granted provided the copyright
 * and "No Warranty" statements are included with all reproductions
 * and derivative works. This software may also be redistributed
 * without charge provided that the copyright and "No Warranty"
 * statements are included in all redistributions.
 *
 * NO WARRANTY. THIS SOFTWARE IS FURNISHED ON AN "AS IS" BASIS.
 * CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER
 * EXPRESSED OR IMPLIED AS TO THE MATTER INCLUDING, BUT NOT LIMITED
 * TO: WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY
 * OF RESULTS OR RESULTS OBTAINED FROM USE OF THIS SOFTWARE. CARNEGIE
 * MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT
 * TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 */


#include <nasd/nasd_options.h>
#include <nasd/nasd_types.h>
#include <nasd/nasd_error.h>
#include <nasd/nasd_common.h>
#include <nasd/nasd_mem.h>
#include <nasd/nasd_general.h>
#include <nasd/nasd_threadstuff.h>
#include <nasd/nasd_freelist.h>
#include <nasd/nasd_nonce_mgmt.h>

int nasd_sec_nonce_use_counter;
NASD_DECLARE_ONCE(nasd_sec_nonce_init_once)
NASD_DECLARE_MUTEX(nasd_sec_nonce_counter_lock)

/* default size of recent-nonce hash table */
#define NASD_SEC_NONCE_BUCKETS 128
/* hash function */
#define NASD_SEC_HASH_NONCE(_mgr_,_el_) \
  (((unsigned int)(((_el_)->time>>8)+(_el_)->ni))\
    %(_mgr_)->nasd_sec_nonce_nbuckets)

/* freelist parameters */
#define NASD_MAX_FREE_NONCE 1024
#define NASD_NONCE_INC        32
#define NASD_NONCE_INITIAL   512
nasd_freelist_t *nasd_sec_nonce_fl;

#define NASD_SEC_NONCE_GET(_e_)	\
	NASD_FREELIST_GET(nasd_sec_nonce_fl, _e_, next, (nasd_sec_nonce_ent_t *))
#define NASD_SEC_NONCE_FREE(_e_) \
	NASD_FREELIST_FREE(nasd_sec_nonce_fl, _e_, next)

#define DEBUG_NONCE_HASH_SIZE		0
#define DEBUG_NONCE_HASH_SIZE_VERBOSE	0

void
nasd_sec_print_table_sizes(
  nasd_sec_nonce_mgr_t  *mgr)
{
  int i;
  nasd_sec_nonce_bucket_t *bucket;
  nasd_sec_nonce_ent_t *ent;
  int cnt;
  unsigned long total, max, min;

  total = max = 0;
  min = (unsigned long) -1;

  for(i = 0; i < mgr->nasd_sec_nonce_nbuckets; i++) {
    bucket = &mgr->nasd_sec_recent_nonces[i];
    NASD_LOCK_MUTEX(bucket->lock);
    cnt = 0;
    for(ent = bucket->head; ent; ent = ent->next) {
      cnt++;
    }
#if DEBUG_NONCE_HASH_SIZE_VERBOSE > 0
    nasd_printf("nonce bucket %03d: %d\n", i, cnt);
#endif /* DEBUG_NONCE_HASH_SIZE_VERBOSE > 0 */
    total += cnt;
    max = NASD_MAX(max, cnt);
    min = NASD_MIN(min, cnt);
    NASD_UNLOCK_MUTEX(bucket->lock);
  }

  nasd_printf("total %d buckets, %ld nonces, max %ld, min %ld, avg %ld\n",
	      mgr->nasd_sec_nonce_nbuckets, total, max, min,
	      total / mgr->nasd_sec_nonce_nbuckets);
}

/* initialize nonce-management subsystem */
nasd_status_t
nasd_sec_real_init_nonce()
{
  NASD_FREELIST_CREATE(nasd_sec_nonce_fl, NASD_MAX_FREE_NONCE,
		       NASD_NONCE_INC, sizeof(nasd_sec_nonce_ent_t));
  if (nasd_sec_nonce_fl == NULL)
    return(NASD_NO_MEM);

  NASD_FREELIST_PRIME(nasd_sec_nonce_fl, NASD_NONCE_INITIAL, next,
		      (nasd_sec_nonce_ent_t *));

  return(NASD_SUCCESS);
}

/* executed exactly once, first time nasd_sec_init_nonce() is called. */
void
nasd_sec_nonce_sysinit(void)
{
  nasd_status_t rc;

  rc = nasd_mutex_init(&nasd_sec_nonce_counter_lock);
  if(rc) {
    NASD_PANIC();
  }

  nasd_sec_nonce_use_counter = 0;
}

nasd_status_t
nasd_sec_init_nonce()
{
  nasd_status_t rc;

  nasd_once(&nasd_sec_nonce_init_once, nasd_sec_nonce_sysinit);
  NASD_LOCK_MUTEX(nasd_sec_nonce_counter_lock);
  nasd_sec_nonce_use_counter++;
  if(nasd_sec_nonce_use_counter == 1) {
    rc = nasd_sec_real_init_nonce();
    if (rc) {
      nasd_sec_nonce_use_counter = 0;
    }
  }
  NASD_UNLOCK_MUTEX(nasd_sec_nonce_counter_lock);
  return rc;
}

/* shut down the nonce subsystem for real. */
void
nasd_sec_real_shutdown_nonce(void)
{
  NASD_FREELIST_DESTROY(nasd_sec_nonce_fl, next,
    (nasd_sec_nonce_ent_t *));
}  

/* called when one system is done using us. */
void
nasd_sec_shutdown_nonce(
  void  *ignored)
{
  NASD_ASSERT(nasd_sec_nonce_use_counter != 0);

  NASD_LOCK_MUTEX(nasd_sec_nonce_counter_lock);
  nasd_sec_nonce_use_counter--;
  if(nasd_sec_nonce_use_counter == 0) {
    nasd_sec_real_shutdown_nonce();
  }
  NASD_UNLOCK_MUTEX(nasd_sec_nonce_counter_lock);
}

/* Perform freshness checks on capability and nonce.  We make the
   caller specify the "current time" so this can be called from both
   the drive and the client. */
nasd_status_t
nasd_sec_check_nonce(
  nasd_sec_nonce_mgr_t  *mgr,
  nasd_capability_t     *capability,
  nasd_byte_t           *cap_key,
  nasd_timespec_t       *nonce,
  nasd_timespec_t       *now)
{
  nasd_uint64 now_l;
  nasd_uint64 ts_l;
  nasd_uint64 diff;
  nasd_sec_nonce_bucket_t *bucket;
  nasd_sec_nonce_ent_t *ne, *np, *next;
  int b;
  nasd_status_t rc;
#if DEBUG_NONCE_HASH_SIZE > 0
  static unsigned long ctr;
#endif /* DEBUG_NONCE_HASH_SIZE > 0 */

  /* convert to unsigned 64-bit quantities to make math easier */
  now_l = (nasd_uint64) now->ts_nsec +
    (nasd_uint64) now->ts_sec*NASD_NSEC_PER_SEC;
  ts_l = (nasd_uint64) nonce->ts_nsec +
    (nasd_uint64) nonce->ts_sec*NASD_NSEC_PER_SEC;

  if(ts_l > now_l)
    diff = ts_l - now_l;
  else
    diff = now_l - ts_l;

  /* peer will set the nonce timestamp to be its idea of our current
     time when it sends the message.  we assume that any request older
     than NASD_ACCEPTABLE_SKEW is a replay. */

  if(diff > NASD_ACCEPTABLE_SKEW) {
#if NASD_SECURITY_DEBUG_ERRORS > 0
    nasd_printf("nasd_sec_check_nonce: skew check failed: nonce=%d.%09d now=%d.%09d, diff=%"
		NASD_64s_FMT " max=%" NASD_64s_FMT "\n",
		nonce->ts_sec, nonce->ts_nsec, now->ts_sec, now->ts_nsec,
		diff, NASD_ACCEPTABLE_SKEW);
#endif /* NASD_SECURITY_DEBUG_ERRORS > 0 */
    return NASD_BAD_NONCE;
  }

  /* get & fill in a new nonce-entry struct */
  NASD_SEC_NONCE_GET(ne);
  bzero(ne, sizeof(nasd_sec_nonce_ent_t));
  ne->time = ts_l;
  if(capability) {
    ne->ni = capability->ni;
    ne->expiration = capability->expiration_seconds;
    bcopy(cap_key, ne->key, sizeof(nasd_key_t));
  }

  /* look for it in our recent-nonce hash table, and insert it if not
     found.  while we walk through this bucket, we also perform GC
     and free any entries that are older than NASD_ACCEPTABLE_SKEW. */
  b = NASD_SEC_HASH_NONCE(mgr,ne);
  NASD_ASSERT(b >= 0);
  bucket = &mgr->nasd_sec_recent_nonces[b];
  NASD_LOCK_MUTEX(bucket->lock);
  rc = NASD_SUCCESS;
  for(np = bucket->head; np; np = next) {
    next = np->next;
    if(np->time == ne->time &&
       np->ni == ne->ni &&
       np->expiration == ne->expiration &&
       bcmp(np->key, ne->key, sizeof(nasd_key_t)) == 0) {
      /* matched -- replay detected */
#if NASD_SECURITY_DEBUG_ERRORS > 0
      nasd_printf("nasd_sec_check_nonce: replay detected\n");
#endif /* NASD_SECURITY_DEBUG_ERRORS > 0 */
      rc = NASD_BAD_NONCE;
    }
    if(NASD_ABS(now_l - np->time) > NASD_ACCEPTABLE_SKEW) {
      /* GC this entry */
      if(np->prev)
	np->prev->next = np->next;
      if(np->next)
	np->next->prev = np->prev;
      if(bucket->head == np)
	bucket->head = np->next;
      bucket->size--;
      NASD_SEC_NONCE_FREE(np);
    }
  }

  if(rc == NASD_SUCCESS) {
    /* add this nonce to the list */
    ne->prev = NULL;
    ne->next = bucket->head;
    if(bucket->head)
      bucket->head->prev = ne;
    bucket->head = ne;
    bucket->size++;
  } else {
    NASD_SEC_NONCE_FREE(ne);
  }

  NASD_UNLOCK_MUTEX(bucket->lock);

#if DEBUG_NONCE_HASH_SIZE > 0
  if(ctr++ % 100 == 0) {
    nasd_sec_print_table_sizes(mgr);
  }
#endif /* DEBUG_NONCE_HASH_SIZE > 0 */

  NASD_ASSERT(ne->next || ne->prev || (bucket->head == ne));

  return NASD_SUCCESS;
}

void
nasd_sec_nonce_mgr_destroy(
  nasd_sec_nonce_mgr_t  *mgr)
{
  nasd_sec_nonce_ent_t *np, *nn;
  int i;

  /* put any nonce entries in the table back on the freelist */
  for(i=0;i<mgr->nasd_sec_nonce_nbuckets;i++) {
    for(np = mgr->nasd_sec_recent_nonces[i].head; np; np = nn) {
      nn = np->next;
      NASD_SEC_NONCE_FREE(np);
    }
    mgr->nasd_sec_recent_nonces[i].head = NULL;
    nasd_mutex_destroy(&mgr->nasd_sec_recent_nonces[i].lock);
  }

  NASD_Free(mgr->nasd_sec_recent_nonces,
    mgr->nasd_sec_nonce_nbuckets * sizeof(nasd_sec_nonce_bucket_t));

  NASD_Free(mgr, sizeof(nasd_sec_nonce_mgr_t));
}

void
nasd_sec_nonce_mgr_shutdown(
  void  *mgr_arg)
{
  nasd_sec_nonce_mgr_t *mgr;

  mgr = (nasd_sec_nonce_mgr_t *)mgr_arg;
  nasd_sec_nonce_mgr_destroy(mgr);
}

nasd_status_t
nasd_sec_nonce_mgr_init(
  nasd_sec_nonce_mgr_t  **mgrp,
  int                     table_size)
{
  nasd_sec_nonce_mgr_t *mgr;
  nasd_status_t rc;
  int i, j;

  *mgrp = NULL;

  NASD_Malloc(mgr, sizeof(nasd_sec_nonce_mgr_t),
    (nasd_sec_nonce_mgr_t *));
  if (mgr == NULL)
    return(NASD_NO_MEM);

  if(table_size)
    mgr->nasd_sec_nonce_nbuckets = table_size;
  else
    mgr->nasd_sec_nonce_nbuckets = NASD_SEC_NONCE_BUCKETS;

  NASD_Malloc(mgr->nasd_sec_recent_nonces,
	      mgr->nasd_sec_nonce_nbuckets * sizeof(nasd_sec_nonce_bucket_t),
	      (nasd_sec_nonce_bucket_t *));
  if (mgr->nasd_sec_recent_nonces == NULL) {
    NASD_Free(mgr, sizeof(nasd_sec_nonce_mgr_t));
    return(NASD_NO_MEM);
  }

  for(i=0;i<mgr->nasd_sec_nonce_nbuckets;i++) {
    rc = nasd_mutex_init(&mgr->nasd_sec_recent_nonces[i].lock);
    if (rc) {
      for(j=0;j<i;j++) {
        rc = nasd_mutex_destroy(&mgr->nasd_sec_recent_nonces[j].lock);
        if (rc) {
          nasd_printf(
            "WARNING: got 0x%x (%s) destroying nonce bucket mutex\n",
            rc, nasd_error_string(rc));
        }
      }
      NASD_Free(mgr->nasd_sec_recent_nonces,
        mgr->nasd_sec_nonce_nbuckets * sizeof(nasd_sec_nonce_bucket_t));
      NASD_Free(mgr, sizeof(nasd_sec_nonce_mgr_t));
      return(rc);
    }
    mgr->nasd_sec_recent_nonces[i].head = NULL;
    mgr->nasd_sec_recent_nonces[i].size = 0;
  }

  *mgrp = mgr;
  return(NASD_SUCCESS);
}
