root/usr/src/cmd/keyserv/keyserv_cache.c
/*
 * 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 1997 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <thread.h>
#include <synch.h>
#include <stdlib.h>
#include <syslog.h>
#include <rpc/des_crypt.h>

#include "keyserv_cache.h"

struct cachekey {
        struct cachekey_header  *ch;
        keylen_t                keylen;
        algtype_t               algtype;
        mutex_t                 mp;
        struct cachekey         *next;
};

static struct cachekey          *cache = 0;
static mutex_t                  cache_lock = DEFAULTMUTEX;
static cond_t                   cache_cv = DEFAULTCV;
static u_long                   cache_refcnt = 0;

struct skck {
        des_block       common[3];
        des_block       verifier;       /* Checksum */
        struct dhkey    secret;
};

struct cachekey_disklist {
        uid_t                           uid;
        struct cachekey_disklist        *prev;          /* LRU order */
        struct cachekey_disklist        *next;
        struct cachekey_disklist        *prevhash;      /* Hash chain */
        struct cachekey_disklist        *nexthash;
        struct dhkey                    public;
        /*
         * Storage for encrypted skck structure here. The length will be
         * 8 * ( ( ( sizeof(struct skck) - 1 + secret.length ) - 1 ) / 8 + 1 )
         */
};

/* Length of skck structure for given key length (in bits) */
#define SKCK_LEN(keylen)        ALIGN8(sizeof (struct skck) + KEYLEN(keylen))
/* Length of a cachekey_disklist record for given key length (in bits) */
#define CACHEKEY_RECLEN(keylen) ALIGN8(sizeof (struct cachekey_disklist) - 1 + \
                                        KEYLEN(keylen) + SKCK_LEN(keylen))
#define NUMHASHBUCKETS  253
#define CHUNK_NUMREC    64

#define CACHEKEY_HEADER_VERSION 0

struct cachekey_header {                /* First in each key cache file */
        u_int           version;        /* version number of interface */
        u_int           headerlength;   /* size of this header */
        keylen_t        keylen;         /* in bits */
        algtype_t       algtype;        /* algorithm type */
        size_t          reclength;      /* cache file record size in bytes */
        int             fd;             /* file descriptor */
        caddr_t         address;        /* mmap()ed here */
        size_t          length;         /* bytes mapped */
        size_t          maxsize;        /* don't grow beyond this */
        u_int           inuse_count;
        struct cachekey_disklist        *inuse; /* LRU order */
        struct cachekey_disklist        *inuse_end;
        u_int                           free_count;
        struct cachekey_disklist        *free;
        struct cachekey_disklist        *bucket[NUMHASHBUCKETS];
        struct cachekey_disklist        array[1];       /* Start of array */
};


static struct cachekey_header   *create_cache_file_ch(keylen_t keylen,
                                                        algtype_t algtype,
                                                        int sizespec);

static struct cachekey_header   *remap_cache_file_ch(struct cachekey_header *ch,
                                                u_int newrecs);

static struct cachekey_header   *cache_insert_ch(struct cachekey_header *ch,
                                                uid_t uid, deskeyarray common,
                                                des_block key,
                                                keybuf3 *public,
                                                keybuf3 *secret);

static struct cachekey3_list    *cache_retrieve_ch(struct cachekey_header *ch,
                                                uid_t uid,
                                                keybuf3 *public,
                                                des_block key);

static int                      cache_remove_ch(struct cachekey_header *ch,
                                                uid_t uid,
                                                keybuf3 *public);

static struct cachekey          *get_cache_header(keylen_t keylen,
                                                        algtype_t algtype);

static void                     release_cache_header(struct cachekey *);

static int                      cache_remap_addresses_ch(
                                        struct cachekey_header *);

static struct cachekey_disklist *find_cache_item(struct cachekey_header **,
                                                uid_t, struct dhkey *);

static struct dhkey             *keybuf3_2_dhkey(keybuf3 *);

static u_int                    hashval(uid_t);

static void                     list_remove(struct cachekey_disklist *,
                                                struct cachekey_disklist **,
                                                struct cachekey_disklist **,
                                                u_int *);

static void                     list_remove_hash(struct cachekey_disklist *,
                                                struct cachekey_disklist **,
                                                struct cachekey_disklist **,
                                                u_int *);

static void                     list_insert(struct cachekey_disklist *,
                                                struct cachekey_disklist **,
                                                struct cachekey_disklist **,
                                                u_int *);

static void                     list_insert_hash(struct cachekey_disklist *,
                                                struct cachekey_disklist **,
                                                struct cachekey_disklist **,
                                                u_int *);

static struct cachekey3_list *  copy_cl_item(struct cachekey_header *ch,
                                                struct cachekey_disklist *cd,
                                                des_block key);

extern int                      hex2bin(u_char *, u_char *, int);
extern int                      bin2hex(u_char *, u_char *, int);

/*
 * The folowing set of macros implement address validity checking. A valid
 * address is defined to be either 0, or to fall on a record boundary. In
 * the latter case, the the difference between the address and the start of
 * the record array is divisible by the record length.
 */
#define FILEOFFSET(ckh)                 ((u_long)(ckh) - \
                                        (u_long)((ckh)->address))
#define ADJUSTEDADDR(addr, ckh)         ((u_long)(addr) + FILEOFFSET(ckh))
#define ARRAYOFFSET(addr, ckh)          (ADJUSTEDADDR(addr, ckh) - \
                                        (u_long)&((ckh)->array[0]))
#define INVALID_ADDRESS(addr, ckh)      ((addr == 0) ? 0 : \
                        (ARRAYOFFSET(addr, ckh) % (ckh)->reclength) != 0)

/* Add offset to old address */
#define MOVE_ADDR(old, offset)  ((old) == 0) ? 0 : \
                                (void *)((u_long)(old) + (offset))

/* Number of records in use or on free list */
#define NUMRECS(ck_header)      ((ck_header)->inuse_count + \
                                (ck_header)->free_count)

/* Max number of records the mapped file could hold */
#define MAPRECS(ck_header)      (((ck_header)->length - \
                                sizeof (struct cachekey_header)) / \
                                (ck_header)->reclength)
/* Max number of records the file will hold if extended to the maxsize */
#define MAXRECS(ck_header)      (((ck_header)->maxsize - \
                                sizeof (struct cachekey_header)) / \
                                (ck_header)->reclength)


struct cachekey_header *
create_cache_file_ch(keylen_t keylen, algtype_t algtype, int sizespec)
{
        char                            filename[MAXPATHLEN];
        struct cachekey_header          *ch;
        int                             fd, newfile = 0, i, checkvalid = 1;
        struct stat                     statbuf;
        size_t                          reclength, length;
        struct cachekey_header          *oldbase = 0;
        struct cachekey_disklist        *cd;
        size_t maxsize;

        /* Construct cache file name */
        if (snprintf(filename, sizeof (filename), "/var/nis/.keyserv_%d-%d",
                        keylen, algtype) > sizeof (filename)) {
                syslog(LOG_WARNING,
                "error constructing file name for mech %d-%d", keylen, algtype);
                return (0);
        }

        /* Open/create the file */
        if ((fd = open(filename, O_RDWR|O_CREAT, 0600)) < 0) {
                syslog(LOG_WARNING, "cache file open error for mech %d-%d: %m",
                        keylen, algtype);
                return (0);
        }

        /* We want exclusive use of the file */
        if (lockf(fd, F_LOCK, 0) < 0) {
                syslog(LOG_WARNING, "cache file lock error for mech %d-%d: %m",
                        keylen, algtype);
                close(fd);
                return (0);
        }

        /* Zero size means a new file */
        if (fstat(fd, &statbuf) < 0) {
                syslog(LOG_WARNING, "cache file fstat error for mech %d-%d: %m",
                        keylen, algtype);
                close(fd);
                return (0);
        }

        reclength = CACHEKEY_RECLEN(keylen);
        if (sizespec < 0) {
                /* specifies the number of records in file */
                maxsize = ALIGN8(sizeof (struct cachekey_header)) +
                        -sizespec*reclength;
        } else {
                /* specifies size of file in MB */
                maxsize = sizespec*1024*1024;
        }
        length    = ALIGN8(sizeof (struct cachekey_header)) +
                        reclength*CHUNK_NUMREC;
        if (length > maxsize) {
                /*
                 * First record resides partly in the header, so the length
                 * cannot be allowed to be less than header plus one record.
                 */
                if (maxsize > ALIGN8(sizeof (struct cachekey_header)+reclength))
                        length = maxsize;
                else {
                        length  = ALIGN8(sizeof (struct cachekey_header)+
                                        reclength);
                        maxsize = length;
                }
        }

        if (statbuf.st_size == 0) {
                /* Extend the file if we just created it */
                if (ftruncate(fd, length) < 0) {
                        syslog(LOG_WARNING,
                                "cache file ftruncate error for mech %d-%d: %m",
                                keylen, algtype);
                        close(fd);
                        return (0);
                }
                newfile = 1;
        } else {
                /*
                 * Temporarily mmap the header, to sanity check and obtain
                 * the address where it was mapped the last time.
                 */
                if ((ch = (void *)mmap(0, sizeof (struct cachekey_header),
                                PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) ==
                        MAP_FAILED) {
                        syslog(LOG_WARNING,
                                "cache file mmap1 error for mech %d-%d: %m",
                                keylen, algtype);
                        close(fd);
                        return (0);
                }
                if (ch->version != CACHEKEY_HEADER_VERSION ||
                        ch->headerlength != sizeof (struct cachekey_header) ||
                        ch->keylen != keylen ||
                        ch->algtype != algtype ||
                        ch->reclength != reclength ||
                        ch->length < sizeof (struct cachekey_header) ||
                        ch->maxsize < ch->length ||
                        INVALID_ADDRESS(ch->inuse, ch) ||
                        INVALID_ADDRESS(ch->free, ch)) {
                        syslog(LOG_WARNING,
                        "cache file consistency error for mech %d-%d",
                                keylen, algtype);
                        munmap((caddr_t)ch, sizeof (struct cachekey_header));
                        close(fd);
                        return (0);
                }
                oldbase = (void *)ch->address;
                length  = ch->length;
                if (munmap((caddr_t)ch, sizeof (struct cachekey_header)) < 0) {
                        syslog(LOG_WARNING,
                                "cache file munmap error for mech %d-%d: %m",
                                keylen, algtype);
                        close(fd);
                        return (0);
                }
        }

        /* Map the file */
        if ((ch = (void *)mmap((caddr_t)oldbase, length,
                PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
                syslog(LOG_WARNING,
                        "cache file mmap2 error for mech %d-%d: %m",
                                keylen, algtype);
                close(fd);
                return (0);
        }

        ch->fd          = fd;
        ch->maxsize     = maxsize;

        if (newfile) {
                ch->version             = CACHEKEY_HEADER_VERSION;
                ch->headerlength        = sizeof (struct cachekey_header);
                ch->keylen              = keylen;
                ch->algtype             = algtype;
                ch->reclength           = reclength;
                ch->length              = length;
                ch->address             = (caddr_t)ch;
                ch->inuse_count         = 0;
                ch->inuse               = 0;
                ch->inuse_end           = 0;
                ch->free                = 0;
                ch->free_count          = 0;
                for (i = 0; i < NUMHASHBUCKETS; i++) {
                        ch->bucket[i] = 0;
                }

                cd = &(ch->array[0]);
                for (i = 0; i < MAPRECS(ch);
                        i++, cd = MOVE_ADDR(cd, ch->reclength)) {
                        cd->uid         = (uid_t)-1;
                        cd->prev        = MOVE_ADDR(cd, -(ch->reclength));
                        cd->next        = MOVE_ADDR(cd, +(ch->reclength));
                        cd->prevhash    = 0;
                        cd->nexthash    = 0;
                }
                /*
                 * Last record next pointer, and first record prev pointer,
                 * are both NULL.
                 */
                cd              = MOVE_ADDR(cd, -(ch->reclength));
                cd->next        = 0;
                cd              = &(ch->array[0]);
                cd->prev        = 0;

                ch->free_count  = MAPRECS(ch);
                ch->free        = &(ch->array[0]);

                (void) msync((caddr_t)ch, ch->length, MS_SYNC);

        } else if (ch->length > maxsize) {
                /* File should shrink */
                if ((ch = remap_cache_file_ch(ch, MAXRECS(ch))) == 0) {
                        return (0);
                }
                checkvalid = 0;
        }

        /*
         * cache_remap_addresses() also checks address consistency, so call
         * it even if the remap is a no-op. However, if we've called
         * remap_cache_file_ch(), it will have invoked cache_remap_addresses()
         * already, so we don't have to do that again.
         */
        if (checkvalid &&
                cache_remap_addresses_ch(ch) == 0) {
                syslog(LOG_WARNING, "cache file invalid for mech %d-%d",
                        keylen, algtype);
                (void) munmap((caddr_t)ch, ch->length);
                close(fd);
                return (0);
        }

        (void) msync((caddr_t)ch, ch->length, MS_SYNC);

        return (ch);
}


static int
cache_remap_addresses_ch(struct cachekey_header *ch)
{
        int                             i;
        u_long                          offset;
        struct cachekey_disklist        *cd;

        offset = (u_long)ch - (u_long)ch->address;

        if (INVALID_ADDRESS(ch->inuse, ch) ||
                INVALID_ADDRESS(ch->inuse_end, ch) ||
                INVALID_ADDRESS(ch->free, ch)) {
                return (0);
        }

        ch->inuse       = MOVE_ADDR(ch->inuse, offset);
        ch->inuse_end   = MOVE_ADDR(ch->inuse_end, offset);
        ch->free        = MOVE_ADDR(ch->free, offset);

        cd = &(ch->array[0]);
        for (i = 0; i < NUMRECS(ch); i++) {
                if (INVALID_ADDRESS(cd->prev, ch) ||
                        INVALID_ADDRESS(cd->next, ch) ||
                        INVALID_ADDRESS(cd->prevhash, ch) ||
                        INVALID_ADDRESS(cd->nexthash, ch)) {
                        return (0);
                }
                cd->prev        = MOVE_ADDR(cd->prev, offset);
                cd->next        = MOVE_ADDR(cd->next, offset);
                cd->prevhash    = MOVE_ADDR(cd->prevhash, offset);
                cd->nexthash    = MOVE_ADDR(cd->nexthash, offset);
                cd = MOVE_ADDR(cd, ch->reclength);
        }

        for (i = 0; i < NUMHASHBUCKETS; i++) {
                if (INVALID_ADDRESS(ch->bucket[i], ch)) {
                        return (0);
                }
                ch->bucket[i] = MOVE_ADDR(ch->bucket[i], offset);
        }

        /*
         * To prevent disaster if this function is invoked again, we
         * update ch->address, so that offset will be zero if we do
         * get called once more, and the mapped file hasn't moved.
         */
        ch->address = (caddr_t)ch;

        return (1);
}


/*
 * Remap cache file with space for 'newrecs' records. The mmap:ed address
 * may have to move; the new address is returned.
 */
static struct cachekey_header *
remap_cache_file_ch(struct cachekey_header *ch, u_int newrecs)
{
        size_t                          newsize, oldsize;
        u_int                           currecs;
        int                             i, fd;
        struct cachekey_header          *newch;
        caddr_t                         oldaddr;
        struct cachekey_disklist        *cd = 0;

        if (ch == 0)
                return (0);


        /*
         * Since the first record partly resides in the cachekey_header,
         * newrecs cannot be less than 1.
         */
        if (newrecs < 1)
                newrecs = 1;

        newsize = ALIGN8(sizeof (struct cachekey_header)) +
                        (ch->reclength)*newrecs;
        currecs = NUMRECS(ch);

        if (newsize > ch->maxsize) {
                /* Would exceed maximum allowed */
                newsize = ch->maxsize;
        }

        /* Save stuff we need while the file is unmapped */
        oldsize = ch->length;
        oldaddr = (caddr_t)ch;
        fd      = ch->fd;

        if (newsize > ch->length) {
                /* Extending the file */
                cd = &(ch->array[0]);
        } else if (newsize == ch->length) {
                /* Already OK */
                return (ch);
        } else {
                size_t                          tmpsize;
                struct cachekey_disklist        *fcd;
                /*
                 * Shrink the file by removing records from the end.
                 * First, we have to make sure the file contains valid
                 * addresses.
                 */
                if (cache_remap_addresses_ch(ch) == 0) {
                        syslog(LOG_WARNING, "cache file invalid for mech %d-%d",
                        ch->keylen, ch->algtype);
                        close(ch->fd);
                        munmap((caddr_t)ch, ch->length);
                        return (0);
                }
                fcd = MOVE_ADDR(&(ch->array[0]),
                                ch->reclength*(MAPRECS(ch)-1));
                tmpsize = (u_long)fcd - (u_long)ch + ch->reclength;
                while (tmpsize > newsize && fcd > &(ch->array[0])) {
                        if (fcd->uid == (uid_t)-1) {
                                list_remove(fcd, &(ch->free), 0,
                                        &(ch->free_count));
                        } else {
                                list_remove_hash(fcd,
                                        &(ch->bucket[hashval(fcd->uid)]), 0, 0);
                                list_remove(fcd, &(ch->inuse), &(ch->inuse_end),
                                                &(ch->inuse_count));
                        }
                        tmpsize -= ch->reclength;
                        fcd = MOVE_ADDR(fcd, -(ch->reclength));
                }
                ch->length = newsize;
                (void) msync((caddr_t)ch, ch->length, MS_SYNC);
        }

        /* Unmap the file */
        if (munmap((caddr_t)oldaddr, oldsize) < 0) {
                return (0);
        }
        ch = 0;

        /* Truncate/extend it */
        if (ftruncate(fd, newsize) < 0) {
                return (0);
        }

        /* Map it again */
        if ((newch = (void *)mmap(oldaddr, newsize,
                        PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) ==
        MAP_FAILED) {
                return (0);
        }

        /* Update with new values */
        newch->length   = newsize;

        if (cache_remap_addresses_ch(newch) == 0) {
                syslog(LOG_WARNING, "cache file invalid for mech %d-%d",
                        newch->keylen, newch->algtype);
                newch->length = oldsize;
                close(newch->fd);
                munmap((caddr_t)newch, newsize);
                return (0);
        }

        /* If extending the file, add new records to the free list */
        if (cd != 0) {
                cd = MOVE_ADDR(&(newch->array[0]), currecs*newch->reclength);
                for (i = currecs; i < MAPRECS(newch); i++) {
                        cd->uid         = (uid_t)-1;
                        list_insert(cd, &(newch->free), 0,
                                        &(newch->free_count));
                        cd              = MOVE_ADDR(cd, newch->reclength);
                }
        }

        (void) msync(newch->address, newch->length, MS_SYNC);

        return (newch);
}


#ifdef DEBUG
void
print_cache_ch(struct cachekey_header *ch)
{
        int                             i, inuse, inuse_err, free, free_err;
        int                             pb;
        struct cachekey_disklist        *cd;

        printf(
"\nkeylen = %d, algtype = %d, version = %d, headerlen = %d, reclen = %d\n",
                ch->keylen, ch->algtype, ch->version, ch->headerlength,
                ch->reclength);
        printf("fd = %d, address = 0x%x, mapped length = %d, maxsize = %d\n",
                ch->fd, ch->address, ch->length, ch->maxsize);
        printf("inuse = %d, free = %d\n", ch->inuse_count, ch->free_count);

        printf("Active hash buckets:\n");

        for (i = 0, inuse = 0, inuse_err = 0; i < NUMHASHBUCKETS; i++) {
                cd = ch->bucket[i];
                pb = -1;
                if (cd != 0) {
                        pb = 0;
                        printf("\t%d: ", i);
                }
                while (cd != 0) {
                        pb++;
                        printf("%d ", cd->uid);
                        if (cd->uid != (uid_t)-1) {
                                inuse++;
                        } else {
                                inuse_err++;
                        }
                        cd = cd->nexthash;
                }
                if (pb >= 0)
                        printf(" (%d)\n", pb);
        }

        printf("\ncounted hash inuse = %d, errors = %d\n", inuse, inuse_err);

        cd = ch->inuse;
        inuse = inuse_err = 0;
        while (cd != 0) {
                if (cd->uid != (uid_t)-1) {
                        inuse++;
                } else {
                        inuse_err++;
                }
                cd = cd->next;
        }
        printf("counted LRU  inuse = %d, errors = %d\n", inuse, inuse_err);

        cd = ch->free;
        free = free_err = 0;
        while (cd != 0) {
                if (cd->uid == (uid_t)-1) {
                        free++;
                } else {
                        free_err++;
                        fprintf(stderr, "free = %d, err = %d, cd->uid = %d\n",
                                free, free_err, cd->uid);
                }
                cd = cd->next;
        }
        printf("counted      free = %d, errors = %d\n", free, free_err);
}

void
print_cache(keylen_t keylen, algtype_t algtype)
{
        struct cachekey *c;

        if ((c = get_cache_header(keylen, algtype)) == 0)
                return;

        if (c->ch == 0) {
                release_cache_header(c);
                return;
        }

        print_cache_ch(c->ch);

        release_cache_header(c);
}
#endif



static u_int
hashval(uid_t uid)
{
        return (uid % NUMHASHBUCKETS);
}


static void
list_remove(
        struct cachekey_disklist *item,
        struct cachekey_disklist **head,
        struct cachekey_disklist **tail,
        u_int *count)
{
        if (item == NULL) return;

        /* Handle previous item, if any */
        if (item->prev == 0)
                *head = item->next;
        else
                item->prev->next = item->next;

        /* Take care of the next item, if any */
        if (item->next != 0)
                item->next->prev = item->prev;

        /* Handle tail pointer, if supplied */
        if (tail != 0 && *tail == item)
                *tail = item->prev;

        item->prev = item->next = 0;
        if (count != 0)
                (*count)--;
}


static void
list_remove_hash(
        struct cachekey_disklist *item,
        struct cachekey_disklist **head,
        struct cachekey_disklist **tail,
        u_int *count)
{
        if (item == NULL) return;

        /* Handle previous item, if any */
        if (item->prevhash == 0)
                *head = item->nexthash;
        else
                item->prevhash->nexthash = item->nexthash;

        /* Take care of the next item, if any */
        if (item->nexthash != 0)
                item->nexthash->prevhash = item->prevhash;

        /* Handle tail pointer, if supplied */
        if (tail != 0 && *tail == item)
                *tail = item->prevhash;

        item->prevhash = item->nexthash = 0;
        if (count != 0)
                (*count)--;
}


static void
list_insert(
        struct cachekey_disklist *item,
        struct cachekey_disklist **head,
        struct cachekey_disklist **tail,
        u_int *count)
{
        if (item == NULL) return;

        /* Insert at tail, if supplied */
        if (tail != 0) {
                item->prev = *tail;
                if (item->prev != 0)
                        item->prev->next = item;
                item->next      = 0;
                *tail           = item;
                if (*head == 0)
                        *head   = item;
        } else {
                item->next = *head;
                if (item->next != 0)
                        item->next->prev = item;
                item->prev      = 0;
                *head           = item;
        }
        if (count != 0)
                (*count)++;
}

static void
list_insert_hash(
        struct cachekey_disklist *item,
        struct cachekey_disklist **head,
        struct cachekey_disklist **tail,
        u_int *count)
{
        if (item == NULL) return;

        /* Insert at tail, if supplied */
        if (tail != 0) {
                item->prevhash = *tail;
                if (item->prevhash != 0)
                        item->prevhash->nexthash = item;
                item->nexthash  = 0;
                *tail           = item;
                if (*head == 0)
                        *head   = item;
        } else {
                item->nexthash  = *head;
                if (item->nexthash != 0)
                        item->nexthash->prevhash = item;
                item->prevhash  = 0;
                *head           = item;
        }
        if (count != 0)
                (*count)++;
}


/*
 * Find the cache item specified by the header, uid, and public key. If
 * no such uid/public item exists, return a pointer to an empty record.
 * In either case, the item returned has been removed from any and all
 * lists.
 */
static struct cachekey_disklist *
find_cache_item(struct cachekey_header **ch, uid_t uid, struct dhkey *public)
{
        u_int                           hash;
        struct cachekey_disklist        *cd;

        hash = hashval(uid);

        if ((ch == NULL) || ((*ch) == NULL)) {
                return (0);
        }
        for (cd = (*ch)->bucket[hash]; cd != 0; cd = cd->nexthash) {
                if (uid == cd->uid &&
                        public->length == cd->public.length &&
                        memcmp(public->key, cd->public.key,
                                cd->public.length) == 0) {
                        list_remove_hash(cd, &((*ch)->bucket[hash]), 0, 0);
                        list_remove(cd, &((*ch)->inuse), &((*ch)->inuse_end),
                                        &((*ch)->inuse_count));
                        return (cd);
                }
        }

        if ((cd = (*ch)->free) != 0) {
                list_remove(cd, &((*ch)->free), 0, &((*ch)->free_count));
                return (cd);
        }

        /* Try to extend the file by CHUNK_NUMREC records */
        if (((*ch) = remap_cache_file_ch(*ch, NUMRECS(*ch)+CHUNK_NUMREC)) == 0)
                return (0);

        /* If the extend worked, there should now be at least one free record */
        if ((cd = (*ch)->free) != 0) {
                list_remove(cd, &((*ch)->free), 0, &((*ch)->free_count));
                return (cd);
        }

        /* Sacrifice the LRU item, if there is one */
        if ((cd = (*ch)->inuse) == 0)
                return (0);

        /* Extract from hash list */
        list_remove_hash(cd, &((*ch)->bucket[hashval(cd->uid)]), 0, 0);
        /* Extract from LRU list */
        list_remove(cd, &((*ch)->inuse), &((*ch)->inuse_end),
                        &((*ch)->inuse_count));

        return (cd);
}


static struct cachekey_header *
cache_insert_ch(
        struct cachekey_header *ch,
        uid_t uid,
        deskeyarray common,
        des_block key,
        keybuf3 *public,
        keybuf3 *secret)
{
        struct cachekey_disklist        *cd;
        struct cachekey_header          *newch;
        int                             i, err;
        struct skck                     *skck;
        des_block                       ivec;
        struct dhkey                    *pk;
        struct dhkey                    *sk;


        if (ch == 0 || uid == (uid_t)-1) {
                return (0);
        }

        if (common.deskeyarray_len > sizeof (skck->common)/sizeof (des_block) ||
                (pk = keybuf3_2_dhkey(public)) == 0 ||
                (sk = keybuf3_2_dhkey(secret)) == 0) {
                return (0);
        }

        newch = ch;
        if ((cd = find_cache_item(&newch, uid, pk)) == 0) {
                free(pk);
                free(sk);
                return (newch);
        }

        /*
         * The item may have been free, or may have been the LRU sacrificial
         * lamb, so reset all fields.
         */
        cd->uid = uid;
        memcpy(&(cd->public), pk, DHKEYSIZE(pk));

        skck = MOVE_ADDR(&(cd->public), DHKEYSIZE(pk));
        for (i = 0; i < common.deskeyarray_len; i++) {
                skck->common[i] = common.deskeyarray_val[i];
        }
        skck->verifier = key;
        memcpy(&(skck->secret), sk, DHKEYSIZE(sk));
        free(pk);
        free(sk);
        memcpy(ivec.c, key.c, sizeof (key.c));
        err = cbc_crypt(key.c, (char *)skck, SKCK_LEN(newch->keylen),
                        DES_ENCRYPT|DES_HW, ivec.c);
        if (DES_FAILED(err)) {
                /* Re-insert on free list */
                list_insert(cd, &(newch->free), 0, &(newch->free_count));
                return (newch);
        }

        /* Re-insert on hash list */
        list_insert_hash(cd, &(newch->bucket[hashval(cd->uid)]), 0, 0);
        /* Insert at end of LRU list */
        list_insert(cd, &(newch->inuse), &(newch->inuse_end),
                        &(newch->inuse_count));

        (void) msync((caddr_t)newch, newch->length, MS_SYNC);

        return (newch);
}


static struct cachekey3_list *
copy_cl_item(struct cachekey_header *ch, struct cachekey_disklist *cd,
                des_block key) {

        struct cachekey3_list           *cl;
        struct skck                     *skck, *skck_cd;
        int                             i, err;
        des_block                       ivec;

        /* Allocate the cachekey3_list structure */
        if ((cl = malloc(CACHEKEY3_LIST_SIZE(ch->keylen))) == 0) {
                return (0);
        }

        /* Allocate skck structure for decryption */
        if ((skck = malloc(SKCK_LEN(ch->keylen))) == 0) {
                free(cl);
                return (0);
        }

        /* Decrypt and check verifier */
        skck_cd = MOVE_ADDR(&(cd->public), DHKEYSIZE(&(cd->public)));
        memcpy(skck, skck_cd, SKCK_LEN(ch->keylen));
        memcpy(ivec.c, key.c, sizeof (ivec.c));
        err = cbc_crypt(key.c, (char *)skck, SKCK_LEN(ch->keylen),
                        DES_DECRYPT|DES_HW, ivec.c);
        if (DES_FAILED(err)) {
                free(cl);
                free(skck);
                return (0);
        }
        if (memcmp(key.c, skck->verifier.c, sizeof (skck->verifier.c)) != 0) {
                free(cl);
                free(skck);
                return (0);
        }

        /* Everything OK; copy values */
        cl->public              = MOVE_ADDR(cl, sizeof (struct cachekey3_list));
        cl->public->keybuf3_val = MOVE_ADDR(cl->public, sizeof (keybuf3));
        cl->secret              = MOVE_ADDR(cl->public->keybuf3_val,
                                        ALIGN4(2*KEYLEN(ch->keylen)+1));
        cl->secret->keybuf3_val = MOVE_ADDR(cl->secret, sizeof (keybuf3));
        cl->deskey.deskeyarray_val =
                                MOVE_ADDR(cl->secret->keybuf3_val,
                                        ALIGN4(2*KEYLEN(ch->keylen)+1));
        bin2hex(cd->public.key, (u_char *)cl->public->keybuf3_val,
                cd->public.length);
        cl->public->keybuf3_len = cd->public.length*2+1;

        bin2hex(skck->secret.key, (u_char *)cl->secret->keybuf3_val,
                skck->secret.length);
        cl->secret->keybuf3_len = skck->secret.length*2+1;
        cl->deskey.deskeyarray_len = sizeof (skck->common)/sizeof (des_block);
        for (i = 0; i < cl->deskey.deskeyarray_len; i++) {
                cl->deskey.deskeyarray_val[i] = skck->common[i];
        }

        cl->refcnt = 0;
        cl->next   = 0;

        free(skck);

        return (cl);

}


static struct cachekey3_list *
cache_retrieve_ch(struct cachekey_header *ch, uid_t uid, keybuf3 *public,
                des_block key) {

        struct cachekey_disklist        *cd;
        struct cachekey3_list           *cl = 0, **cltmp = &cl;
        u_int                           hash;
        struct dhkey                    *pk = 0;

        if (uid == (uid_t)-1 ||
                (public != 0 && (pk = keybuf3_2_dhkey(public)) == 0)) {
                return (0);
        }

        hash = hashval(uid);

        for (cd = ch->bucket[hash]; cd != 0; cd = cd->nexthash) {
                if (uid == cd->uid) {
                        /* Match on public key as well ? */
                        if (pk != 0) {
                                if (memcmp(cd->public.key, pk->key,
                                                cd->public.length) != 0) {
                                        /* Keep looking... */
                                        continue;
                                }
                                cl = copy_cl_item(ch, cd, key);
                                /* Match on public key => nothing more to do */
                                break;
                        }
                        *cltmp = copy_cl_item(ch, cd, key);
                        if (*cltmp == 0) {
                                /* Return what we've got */
                                break;
                        }
                        cltmp = &((*cltmp)->next);
                        /* On to the next item */
                }
        }

        if (pk != 0)
                free(pk);

        return (cl);
}


/*
 * Remove specified item. 'public' == 0 => remove all items for uid.
 * Return number of items removed.
 */
static int
cache_remove_ch(struct cachekey_header *ch, uid_t uid, keybuf3 *public) {

        struct cachekey_disklist        *cd, *cdtmp;
        u_int                           hash;
        int                             match = 0;
        struct dhkey                    *pk = 0;

        if (uid == (uid_t)-1 ||
                (public != 0 && (pk = keybuf3_2_dhkey(public)) == 0)) {
                return (0);
        }

        hash = hashval(uid);

        for (cd = ch->bucket[hash]; cd != 0; ) {
                if (uid == cd->uid) {
                        /* Match on public key as well ? */
                        if (pk != 0) {
                                if (memcmp(cd->public.key, pk->key,
                                                cd->public.length) != 0) {
                                        /* Keep looking... */
                                        continue;
                                }
                                match++;
                                list_remove_hash(cd, &(ch->bucket[hash]), 0, 0);
                                list_remove(cd, &(ch->inuse), &(ch->inuse_end),
                                                &(ch->inuse_count));
                                cd->uid = (uid_t)-1;
                                list_insert(cd, &(ch->free), 0,
                                                &(ch->free_count));
                                /* Match on public key => nothing more to do */
                                break;
                        }
                        match++;
                        /*
                         * XXX: Assume that the order of the hash list remains
                         * the same after removal of an item. If this isn't
                         * true, we really should start over from the start
                         * of the hash bucket.
                         */
                        cdtmp = cd->nexthash;
                        list_remove_hash(cd, &(ch->bucket[hash]), 0, 0);
                        list_remove(cd, &(ch->inuse), &(ch->inuse_end),
                                        &(ch->inuse_count));
                        cd->uid = (uid_t)-1;
                        list_insert(cd, &(ch->free), 0,
                                        &(ch->free_count));
                        /* On to the next item */
                        cd = cdtmp;
                } else {
                        cd = cd->nexthash;
                }
        }

        free(pk);
        return (match);
}


#define INCCACHEREFCNT  mutex_lock(&cache_lock); \
                        cache_refcnt++; \
                        mutex_unlock(&cache_lock)

#if !defined(lint) && !defined(__lint)
#define DECCACHEREFCNT  mutex_lock(&cache_lock); \
                        if (cache_refcnt > 0) \
                                if (cache_refcnt-- == 0) (void) cond_broadcast(&cache_cv); \
                        mutex_unlock(&cache_lock)
#else
#define DECCACHEREFCNT  mutex_lock(&cache_lock); \
                        if (cache_refcnt-- == 0) (void) cond_broadcast(&cache_cv); \
                        mutex_unlock(&cache_lock)
#endif

/*
 * Return the cachekey structure for the specified keylen and algtype.
 * When returned, the lock in the structure has been activated. It's the
 * responsibility of the caller to unlock it by calling release_cache_header().
 */
static struct cachekey *
get_cache_header(keylen_t keylen, algtype_t algtype) {

        struct cachekey         *c;

        INCCACHEREFCNT;

        for (c = cache; c != 0; c = c->next) {
                if (c->keylen == keylen && c->algtype == algtype) {
                        mutex_lock(&c->mp);
                        return (c);
                }
        }

        /* Spin until there are no cache readers */
        mutex_lock(&cache_lock);
#if !defined(lint) && !defined(__lint)
        if (cache_refcnt > 0)
#endif
                cache_refcnt--;
        while (cache_refcnt != 0) {
                (void) cond_wait(&cache_cv, &cache_lock);
        }

        if ((c = malloc(sizeof (struct cachekey))) != 0) {
                c->ch           = 0;
                c->keylen       = keylen;
                c->algtype      = algtype;
                mutex_init(&c->mp, 0, 0);
                c->next         = cache;
                cache           = c;
                mutex_lock(&c->mp);
                cache_refcnt++;
                mutex_unlock(&cache_lock);
                return (c);
        }

        mutex_unlock(&cache_lock);
        return (0);
}


static void
release_cache_header(struct cachekey *ck) {

        struct cachekey *c;

        if (ck == 0)
                return;

        for (c = cache; c != 0; c = c->next) {
                if (c == ck) {
                        mutex_unlock(&c->mp);
                        DECCACHEREFCNT;
                        break;
                }
        }
}


int
create_cache_file(keylen_t keylen, algtype_t algtype, int sizespec)
{
        struct cachekey *c;
        int             ret;

        if ((c = get_cache_header(keylen, algtype)) == 0)
                return (0);

        if (c->ch != 0) {
                /* Already created and opened */
                release_cache_header(c);
                return (1);
        }

        ret = (c->ch = create_cache_file_ch(keylen, algtype, sizespec)) != 0;
        release_cache_header(c);

        return (ret);
}


int
cache_insert(
        keylen_t keylen,
        algtype_t algtype,
        uid_t uid,
        deskeyarray common,
        des_block key,
        keybuf3 *public,
        keybuf3 *secret)
{
        struct cachekey *c;
        int             ret;

        if ((c = get_cache_header(keylen, algtype)) == 0)
                return (0);

        if (c->ch == 0) {
                release_cache_header(c);
                return (0);
        }

        ret = (c->ch =
                cache_insert_ch(c->ch, uid, common, key, public, secret)) != 0;

        release_cache_header(c);

        return (ret);
}


struct cachekey3_list *
cache_retrieve(
        keylen_t keylen,
        algtype_t algtype,
        uid_t uid,
        keybuf3 *public,
        des_block key)
{
        struct cachekey         *c;
        struct cachekey3_list   *cl;

        if ((c = get_cache_header(keylen, algtype)) == 0)
                return (0);

        if (c->ch == 0) {
                release_cache_header(c);
                return (0);
        }

        cl = cache_retrieve_ch(c->ch, uid, public, key);

        release_cache_header(c);

        return (cl);
}

int
cache_remove(keylen_t keylen, algtype_t algtype, uid_t uid, keybuf3 *public)
{
        struct cachekey *c;
        int             ret;

        if ((c = get_cache_header(keylen, algtype)) == 0)
                return (0);

        if (c->ch == 0) {
                release_cache_header(c);
                return (0);
        }

        ret = cache_remove_ch(c->ch, uid, public);

        release_cache_header(c);

        return (ret);
}


static struct dhkey *
keybuf3_2_dhkey(keybuf3 *hexkey)
{
        struct dhkey    *binkey;

        /* hexkey->keybuf3_len*4 is the key length in bits */
        if ((binkey = malloc(DHKEYALLOC(hexkey->keybuf3_len*4))) == 0)
                return (0);

        /* Set to zero to keep dbx and Purify access checking happy */
        memset(binkey, 0, DHKEYALLOC(hexkey->keybuf3_len*4));

        binkey->length = hexkey->keybuf3_len/2;
        hex2bin((u_char *)hexkey->keybuf3_val, binkey->key,
                (int)binkey->length);

        return (binkey);
}