/*
 * Copyright (c) 2003-2012
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*
 * An implementation of RFC 4226, "HOTP: An HMAC-Based One-Time Password
 * Algorithm" and Internet-Draft "TOTP: Time-based One-time Password Algorithm"
 * (March 8, 2010).
 *
 * Note that unlike the other kinds of accounts, these accounts may be
 * modified at authentication time as well during account management.
 * An active account file must therefore be write-locked in both circumstances.
 *
 * XXX we currently make some assumptions that we should not, such as that
 * sizeof(Auth_token_counter) >= TOKEN_HOTP_COUNTER_LENGTH
 *
 * XXX could implement bi-directional (mutual) authentication
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: auth_token.c 2586 2012-03-15 16:21:40Z brachman $";
#endif

#include "dacs.h"
#include "dacs_api.h"
#include "frame.h"

static MAYBE_UNUSED const char *log_module_name = "dacstoken";

typedef struct Auth_hotp_param {
  char *counter_str;
} Auth_hotp_param;

typedef struct Auth_totp_param {
  unsigned int time_step;
} Auth_totp_param;

typedef struct Auth_token_param {
  Auth_token_mode mode;
  char *digest_name;
  char *item_type;
  char *username;
  char *serial;
  char *key_str;
  char *pin;
  unsigned int ndigits;
  unsigned int base;
  union {
	Auth_hotp_param *hotp;
	Auth_totp_param *totp;
  } device;
} Auth_token_param;

#ifdef NOTDEF
/* Token provisioning and account import/export. */
typedef struct Auth_token_attrs {
  Strnum type;
  char *name;
  void *value;
} Auth_token_attrs;

static Auth_token_attrs auth_token_attrs[] = {
  { STRNUM_STR,    "u",  NULL },
  { STRNUM_STR,    "d",  NULL },
  { STRNUM_STR,    "en", NULL },
  { STRNUM_STR,    "s",  NULL },
  { STRNUM_STR,    "k",  NULL },
  { STRNUM_STR,    "dn", NULL },
  { STRNUM_STR,    "c",  NULL },
  { STRNUM_STR,    "p",  NULL },
  { STRNUM_UINZ,   "nd", NULL },
  { STRNUM_UINZ,   "b",  NULL },
  { STRNUM_I,      "dr", NULL },
  { STRNUM_STR,    "ph", NULL },
  { STRNUM_TIME_T, "lu", NULL },
  { STRNUM_ERR,    NULL, NULL }
};

typedef struct Auth_token_prov {
  char *username;			/* u */
  char *device;				/* d */
  char *enabled;			/* en */
  char *serial;				/* s */
  char *key_str;			/* k */
  char *digest_name;		/* dn */
  char *counter_str;		/* c */
  char *pin_str;			/* p */
  unsigned int ndigits;		/* nd */
  unsigned int base;		/* b */
  int drift;				/* dr */
  char *pin_hash;			/* ph */
  time_t last_update;		/* lu */
} Auth_token_prov;
#endif

#define ENC(STR)	xml_attribute_value_encode(STR, (int) '\'')
#define DEC(STR)	(STR == NULL ? NULL : xml_attribute_value_decode(STR))

#ifndef PROG

#define ITEM_TYPE_AUTH_TOKEN_DEMO			"auth_token_demo"
#define ITEM_TYPE_AUTH_HOTP_TOKEN_DEMO		"auth_token_hotp_demo"
#define ITEM_TYPE_AUTH_TOTP_TOKEN_DEMO		"auth_token_totp_demo"

static char *auth_token_item_type = ITEM_TYPE_AUTH_TOKEN;
static char *inkeys_item_type = ITEM_TYPE_AUTH_TOKEN_KEYS;
static char *outkeys_item_type = ITEM_TYPE_AUTH_TOKEN_KEYS;
static unsigned int pin_length = 0;
static int auth_token_debug = 0;

static inline int
token_mode(Auth_token_mode mode, Auth_token_mode auth_mode)
{

  return((mode & TOKEN_MODE_MASK) == auth_mode);
}

static inline int
token_enabled(Auth_token_mode mode)
{

  return((mode & TOKEN_MODE_DISABLED) == 0);
}

static char *
token_format_counter(unsigned char *counter)
{
  char *str;

  str = strbtohex(counter, TOKEN_HOTP_COUNTER_LENGTH, 0);

  return(str);
}

/*
 * Increment COUNTER, wrapping around to zero if necessary.
 * Return 0 normally, 1 if the counter has overflowed back to zero.
 */
static int
token_inc_counter(unsigned char *counter)
{
  int i;

  for (i = TOKEN_HOTP_COUNTER_LENGTH - 1; i >= 0; i--) {
	if (++counter[i] != 0)
	  break;
  }

  /* Simply wrap around to zero on overflow. */

  return(i >= 0 ? 0 : 1);
}

/*
 * Decrement COUNTER.
 * Return 0 normally, 1 if the counter has underflowed to the largest value.
 */
static int
token_dec_counter(unsigned char *counter)
{
  int i;

  for (i = TOKEN_HOTP_COUNTER_LENGTH - 1; i >= 0; i--) {
	if (--counter[i] != 255)
	  break;
  }

  /* Simply wrap around to zero on underflow. */

  return(i >= 0 ? 0 : 1);
}

static unsigned char *
token_copy_counter(unsigned char *src, unsigned char *dst)
{
  unsigned char *d;

  if (dst == NULL)
	d = (unsigned char *) malloc(TOKEN_HOTP_COUNTER_LENGTH);
  else
	d = dst;

  memcpy(d, src, TOKEN_HOTP_COUNTER_LENGTH);

  return(d);
}

static unsigned char *
token_set_counter(unsigned char *counter, Auth_token_counter value)
{
  int cbyte, vbits;
  unsigned char *dst;
  Auth_token_counter b;

  if (sizeof(value) < TOKEN_HOTP_COUNTER_LENGTH)
	return(NULL);

  if (counter == NULL)
	dst = (unsigned char *) malloc(TOKEN_HOTP_COUNTER_LENGTH);
  else
	dst = counter;
  memset(dst, 0, TOKEN_HOTP_COUNTER_LENGTH);

  vbits = sizeof(value) * 8;
  cbyte = TOKEN_HOTP_COUNTER_LENGTH - 1;
  for (b = 1; b < vbits; b <<= 1) {
	if (value & b) {
	  if (cbyte < 0)
		return(NULL);
	  dst[cbyte] |= b;
	}

	if ((b % 8) == 0)
	  cbyte--;
  }

  return(dst);
}

/*
 * The counter is represented internally as an 8-byte value.
 * Externally it is represented either as a decimal number or, if prefixed
 * by "0x" or "0X", a hex number; the latter is preferred.
 * If we are given something shorter than 8 bytes, we pad with leading zeroes.
 */
static char *
canonicalize_counter(char *counter_str)
{
  char *cstr, *p;
  size_t clen;

  p = counter_str;
  if (p != NULL) {
	if (*p == '0' && tolower((int) *(p + 1)) == 'x') {
	  /* A hex value. */
	  p += 2;
	  if (!is_strclass(p, STRCLASS_HEX)) {
		log_msg((LOG_ERROR_LEVEL, "Invalid hex counter value"));
		return(NULL);
	  }
	}
	else {
	  uintmax_t cval;

	  /* A decimal value. */
	  if (strnum(counter_str, STRNUM_UINTMAX_T, &cval) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid decimal counter value: %s",
				 counter_str));
		return(NULL);
	  }
	  p = ds_xprintf("%jx", cval);
	}
  }

  if (p == NULL || (clen = strlen(p)) < (TOKEN_HOTP_COUNTER_LENGTH * 2)) {
	Ds ds;

	if (p == NULL)
	  clen = 0;

	ds_init(&ds);
	while (clen < (TOKEN_HOTP_COUNTER_LENGTH * 2)) {
	  ds_asprintf(&ds, "0");
	  clen++;
	}

	if (p != NULL)
	  ds_asprintf(&ds, "%s", p);
	cstr = ds_buf(&ds);
  }
  else if (clen > (TOKEN_HOTP_COUNTER_LENGTH * 2)) {
	log_msg((LOG_ERROR_LEVEL, "Invalid counter value"));
	return(NULL);
  }
  else
	cstr = p;

  return(cstr);
}

/*
 * Token import/export (provisioning)
 * Create accounts by reading <filename>, one account per line
 * <!ELEMENT otp_token EMPTY>
 * <!ATTLIST otp_token
 *   username     CDATA   #REQUIRED			-- u
 *   device (c | t)       #REQUIRED			-- d
 *   serial       CDATA   #REQUIRED			-- s
 *   key          CDATA   #REQUIRED			-- k
 *   ndigits      CDATA   #REQUIRED         -- nd
 *   base         CDATA   #REQUIRED         -- b
 *   enabled (0 | 1)      #IMPLIED			-- en
 *   digest_name  CDATA   #IMPLIED			-- dn
 *   counter      CDATA   #IMPLIED			-- c
 *   time_step    CDATA   #IMPLIED			-- ts
 *   drift        CDATA   #IMPLIED			-- dr
 *   pin          CDATA   #IMPLIED			-- p
 * >
 *
 * Also see Portable Symmetric Key Container (PSKC)
 * http://datatracker.ietf.org/doc/draft-ietf-keyprov-pskc/
 */

#ifdef OLD
/*
 * Flatten (serialize) a token.
 * The formatted result is:
 *   <mode>,<serial-number>,<counter>,<key>,<pin-hash>,<last-update>
 */
static char *
auth_token_old_flatten(Auth_token *token)
{
  char *e_str;
  unsigned int e_len;
  unsigned char *encrypted;
  Ds ds;
  static Crypt_keys *keys = NULL;

  ds_init(&ds);
  ds_asprintf(&ds, "%u", token->mode);
  ds_asprintf(&ds, ",%s", token->serial);
  ds_asprintf(&ds, ",%s", token_format_counter(token->counter));

  if (keys == NULL
	  && (keys = crypt_keys_from_vfs(outkeys_item_type)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot load keys using %s", outkeys_item_type));
	return(NULL);
  }
  e_len = crypto_encrypt_string(keys, (unsigned char *) token->key,
								token->keylen, &encrypted);
  strba64(encrypted, e_len, &e_str);
  ds_asprintf(&ds, ",%s", e_str);

  if (token->pin_hash == NULL)
	ds_asprintf(&ds, ",0");
  else
	ds_asprintf(&ds, ",%s", token->pin_hash);
  ds_asprintf(&ds, ",%u", token->last_update);

  return(ds_buf(&ds));
}
#endif

static Auth_token *
auth_token_old_unflatten(char *token_str)
{
  char *p, *pstr;
  unsigned int e_len;
  unsigned char *e_str;
  Auth_token *token;
  Dsvec *dsv;
  static Crypt_keys *keys = NULL;

  if (token_str == NULL)
	return(NULL);

  token = ALLOC(Auth_token);
  if ((p = strchr(token_str, (int) ':')) == NULL)
	return(NULL);
  token->username = strndup(token_str, p - token_str);
  p++;
  dsv = strsplit(p, ",", 0);

  token->item_type = "";
  token->mode = TOKEN_MODE_COUNTER;
  token->digest_name = "SHA1";
  token->time_step = 0;
  token->drift = 0;
  token->ndigits = TOKEN_HOTP_NDIGITS;
  token->base = TOKEN_HOTP_BASE;
  strnum((char *) dsvec_ptr_index(dsv, 0), STRNUM_I, &token->mode);
  token->serial = (char *) dsvec_ptr_index(dsv, 1);
  p = (char *) dsvec_ptr_index(dsv, 2);
  if ((token->counter = strhextob(canonicalize_counter(p), NULL)) == NULL)
	return(NULL);

  if (stra64b((char *) dsvec_ptr_index(dsv, 3), &e_str, &e_len) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Base-64 decoding of account failed"));
	return(NULL);
  }

  if (keys == NULL
	  && (keys = crypt_keys_from_vfs(inkeys_item_type)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot load keys using %s", inkeys_item_type));
	return(NULL);
  }
  if (crypto_decrypt_string(keys, e_str, e_len, &token->key, &token->keylen)
	  == -1) {
	log_msg((LOG_ERROR_LEVEL, "Decryption of account failed"));
	return(NULL);
  }

  pstr = (char *) dsvec_ptr_index(dsv, 4);
  if (streq(pstr, "0"))
	token->pin_hash = NULL;
  else
	token->pin_hash = pstr;

  strnum((char *) dsvec_ptr_index(dsv, 5), STRNUM_TIME_T, &token->last_update);

  return(token);
}

/*
 * Flatten (serialize) a token for output.
 * The format is an XML element:
 * <!ELEMENT otp_account EMPTY>
 * <!ATTLIST otp_account
 *   mode         CDATA #REQUIRED
 *   digest_name  CDATA #REQUIRED
 *   serial       CDATA #REQUIRED
 *   enc_key      CDATA #REQUIRED
 *   ndigits      CDATA #REQUIRED
 *   base         CDATA #REQUIRED
 *   time_step    CDATA #IMPLIED
 *   drift        CDATA #IMPLIED
 *   counter      CDATA #IMPLIED
 *   pin_hash     CDATA #IMPLIED
 *   last_update  CDATA #REQUIRED
 * >
 */
static Ds *
auth_token_flatten(Auth_token *token)
{
  char *e_str;
  unsigned int e_len;
  unsigned char *encrypted;
  Ds *ds;
  static Crypt_keys *keys = NULL;

  ds = ds_init(NULL);
  ds_asprintf(ds, "<auth_token");
  ds_asprintf(ds, " u='%s'", ENC(token->username));
  ds_asprintf(ds, " en='%s'", token_enabled(token->mode) ? "1" : "0");
  ds_asprintf(ds, " t='%s'",
			  token_mode(token->mode, TOKEN_MODE_COUNTER) ? "c" : "t");
  ds_asprintf(ds, " dn='%s'", token->digest_name);
  ds_asprintf(ds, " s='%s'", ENC(token->serial));
  if (keys == NULL
	  && (keys = crypt_keys_from_vfs(outkeys_item_type)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot load keys using %s", outkeys_item_type));
	return(NULL);
  }
  e_len = crypto_encrypt_string(keys, (unsigned char *) token->key,
								token->keylen, &encrypted);
  /* Don't free the keys, which may be reused in a subsequent call. */
  strba64(encrypted, e_len, &e_str);
  ds_asprintf(ds, " ek='%s'", e_str);

  ds_asprintf(ds, " nd='%u'", token->ndigits);
  ds_asprintf(ds, " b='%u'", token->base);
  ds_asprintf(ds, " ts='%u'", token->time_step);
  ds_asprintf(ds, " dr='%d'", token->drift);
  if (token->counter != NULL)
	ds_asprintf(ds, " c='0x%s'", token_format_counter(token->counter));
  if (token->pin_hash != NULL)
	ds_asprintf(ds, " ph='%s'", ENC(token->pin_hash));
  ds_asprintf(ds, " lu='%u'", token->last_update);
  ds_asprintf(ds, "/>");

  return(ds);
}

/*
 * Parse the external XML format.
 */
static Auth_token *
auth_token_unflatten(char *token_str, char *item_type)
{
  int mode;
  char *el_name, *endptr, *key_str, *last_update, *p;
  unsigned int e_len;
  unsigned char *e_str;
  Auth_token *token;
  Kwv *kwv;
  Kwv_xml *xml;
  static Crypt_keys *keys = NULL;

  if (token_str == NULL)
	return(NULL);

  if ((xml = kwv_xml_parse(NULL, token_str, &endptr)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Parse error"));
	return(NULL);
  }
  if (!streq(xml->el_name, "auth_token") && xml->type != KWV_XML_EMPTY_TAG) {
	log_msg((LOG_ERROR_LEVEL, "Parse error, invalid element"));
	return(NULL);
  }

  kwv = xml->kwv_attrs;
  mode = 0;
  if ((p = kwv_lookup_value(kwv, "t")) == NULL)
	return(NULL);
  if (streq(p, "c"))
	mode |= TOKEN_MODE_COUNTER;
  else if (streq(p, "t"))
	mode |= TOKEN_MODE_TIME;
  else
	return(NULL);

  if ((p = kwv_lookup_value(kwv, "en")) == NULL)
	return(NULL);
  if (streq(p, "0"))
	mode |= TOKEN_MODE_DISABLED;
  else if (!streq(p, "1"))
	return(NULL);

  token = ALLOC(Auth_token);
  if ((token->username = DEC(kwv_lookup_value(kwv, "u"))) == NULL)
	return(NULL);
  token->item_type = item_type;

  if ((token->digest_name = kwv_lookup_value(kwv, "dn")) == NULL)
	return(NULL);

  if (token_mode(mode, TOKEN_MODE_COUNTER)) {
	if ((p = kwv_lookup_value(kwv, "c")) == NULL)
	  return(NULL);
	if ((token->counter = strhextob(canonicalize_counter(p), NULL)) == NULL)
	  return(NULL);
	token->time_step = 0;
	token->drift = 0;
  }
  else {
	token->counter = NULL;
	if ((p = kwv_lookup_value(kwv, "ts")) == NULL)
	  return(NULL);
	if (strnum(p, STRNUM_UINZ, &token->time_step) == -1)
	  return(NULL);
	if ((p = kwv_lookup_value(kwv, "dr")) == NULL)
	  return(NULL);
	if (strnum(p, STRNUM_I, &token->drift) == -1)
	  return(NULL);
  }
  token->mode = mode;

  token->serial = DEC(kwv_lookup_value(kwv, "s"));
  kwv_lookup_strnum(kwv, "nd", STRNUM_UI, &token->ndigits);
  kwv_lookup_strnum(kwv, "b", STRNUM_UI, &token->base);
  token->serial = kwv_lookup_value(kwv, "s");

  if ((key_str = kwv_lookup_value(kwv, "ek")) == NULL)
	return(NULL);
  if (stra64b(key_str, &e_str, &e_len) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Base-64 decoding of account failed"));
	return(NULL);
  }
  if (keys == NULL
	  && (keys = crypt_keys_from_vfs(inkeys_item_type)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot load keys using %s", inkeys_item_type));
	return(NULL);
  }
  if (crypto_decrypt_string(keys, e_str, e_len, &token->key, &token->keylen)
	  == -1) {
	log_msg((LOG_ERROR_LEVEL, "Decryption of account failed"));
	return(NULL);
  }
  /* We don't free the keys because they will probably be needed again... */

  token->pin_hash = DEC(kwv_lookup_value(kwv, "ph"));
  if ((last_update = kwv_lookup_value(kwv, "lu")) == NULL)
	return(NULL);
  if (strnum(last_update, STRNUM_TIME_T, &token->last_update) == -1)
	return(NULL);

  return(token);
}

/*
 * Convert the raw hash value into the OTP and return the formatted string of
 * NDIGITS (don't suppress leading zeroes) in BASE.
 * Return the binary value as HOTP, if non-NULL.
 * Return NULL if an argument is invalid.
 */
static char *
hotp_value(unsigned int snum, unsigned int ndigits, unsigned int base,
		   unsigned int *hotp)
{
  unsigned int h, m;
  char *value;
  static Ds *ds = NULL;

  if (ds == NULL)
	ds = ds_init(NULL);
  else
	ds_reset(ds);

  if (base == 10) {
	/* RFC 4226 E.1 */
	if (ndigits == 6)
	  m = 1000000;		/* 10^6 */
	else if (ndigits == 7)
	  m = 10000000;		/* 10^7 */
	else if (ndigits == 8)
	  m = 100000000;	/* 10^8 */
	else if (ndigits == 9)
	  m = 1000000000;	/* 10^9 */
	else
	  return(NULL);

	h = snum % m;
	if (auth_token_debug)
	  fprintf(stderr, "hotp=%0x\n", h);
	if (hotp != NULL)
	  *hotp = h;

	ds_asprintf(ds, "%0*u", ndigits, h);
  }	else if (base == 16)
	ds_asprintf(ds, "%08X", snum);
  else if (base == 32) {
	unsigned int d, div;
	static const char *alnum32 =
	  "0123456789ABCDEFGHJKMNPRSTUVWXYZ";

	if (ndigits == 6)
	  div = 1073741824;		/* 32^6 */
	else
	  return(NULL);

	h = snum % div;
	if (hotp != NULL)
	  *hotp = h;

	/* RFC 4226 E.2 */
	div /= 32;
	while (div) {
	  d = h / div;
	  ds_appendc(ds, (int) alnum32[d]);
	  h -= div * d;
	  div /= 32;
	}
	ds_appendc(ds, (int) '\0');
  }
  else
	return(NULL);

  value = ds_buf(ds);
  return(value);
}

static unsigned int
hotp_snum(Hmac_digest *hd, unsigned char *key, unsigned int klen,
		  unsigned char *value, unsigned int vlen,
		  unsigned char **hash, unsigned int *hlen)
{
  int sbits;
  unsigned int snum;
  unsigned char *outp;

  outp = crypto_hmac(hd->name, key, klen, value, vlen, hash, hlen);
  if (auth_token_debug)
	fprintf(stderr, "outp=%s\n", strbtohex(outp, CRYPTO_HMAC_BYTE_LENGTH, 0));

  sbits = outp[hd->hmac_size - 1] & 0x0f;
  snum = (outp[sbits] & 0x7f) << 24
	| (outp[sbits + 1] & 0xff) << 16
	| (outp[sbits + 2] & 0xff) << 8
	| (outp[sbits + 3] & 0xff);

  free(outp);
  if (auth_token_debug)
	fprintf(stderr, "snum=%8x %u\n", snum, snum);

  return(snum);
}

/*
 * Update the account record for TOKEN.
 * Return 0 if ok, -1 if an error occurs.
 */
static int
auth_token_set(Auth_token *token)
{
  char *token_str;
  Ds *ds;
  Vfs_handle *h;
  
  log_msg((LOG_TRACE_LEVEL, "Updating token account (%s)...",
		   token->item_type));
  if ((h = vfs_open_item_type(token->item_type)) == NULL)
	return(-1);

  time(&token->last_update);

  ds = auth_token_flatten(token);

  if (vfs_put(h, token->username, (void *) ds_buf(ds), ds_len(ds)) == -1) {
	vfs_close(h);
	return(-1);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_close() failed"));

  log_msg((LOG_DEBUG_LEVEL, "Token account was updated"));
  return(0);
}

static char *
make_pin_hash(char *pin)
{
  char *digest_name, *hval;
  Passwd_digest_alg alg;

  /* XXX */
  digest_name = NULL;
  if (passwd_get_digest_algorithm(&digest_name, &alg) == -1) {
	log_msg((LOG_ERROR_LEVEL,
			 "An unrecognized digest method is configured"));
	return(NULL);
  }
  if ((hval = passwd_make_digest(alg, pin, NULL)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Failed to create digest"));
	return(NULL);
  }

  return(hval);
}

static int
set_pin(Auth_token *token, char *new_pin, char **errmsg)
{
  char *pin_constraints;

  if ((pin_constraints = conf_val(CONF_PASSWORD_CONSTRAINTS)) != NULL) {
	if (!pw_is_passwd_acceptable(new_pin, pin_constraints)) {
	  *errmsg = "PIN does not meet constraints";
	  return(-1);
	}
  }

  if ((token->pin_hash = make_pin_hash(new_pin)) == NULL) {
	*errmsg = "Cannot make pin hash";
	return(-1);
  }

  if (auth_token_set(token) == -1) {
	*errmsg = ds_xprintf("Could not set account for \"%s\"", token->username);
	return(-1);
  }

  return(0);
}

static int
auth_token_set_pin(char *item_type, char *username, char *new_pin,
				   char **errmsg)
{
  int st;
  Auth_token *token;

  if ((token = auth_token_get(item_type, username)) == NULL) {
	*errmsg = ds_xprintf("Cannot find username: \"%s\"", username);
	return(-1);
  }

  st = set_pin(token, new_pin, errmsg);

  return(st);
}

/*
 * Generic hash-based one-time password computation.
 */
char *
auth_token(char *digest_name, unsigned char *key, unsigned int klen,
		   unsigned char *value, unsigned int vlen, unsigned int ndigits,
		   unsigned int base)
{
  unsigned int hotp, snum;
  char *hval;
  Hmac_digest *hd;

  if ((hd = hmac_lookup_digest_by_name(digest_name)) == NULL)
	return(NULL);

  snum = hotp_snum(hd, key, klen, value, vlen, NULL, NULL);
  hval = hotp_value(snum, ndigits, base, &hotp);

  return(hval);
}

/*
 * Return the current counter or interval and the corresponding OTP.
 * For HOTP, the counter is advanced.
 * This can be used to implement mutual authentication.
 */
int
auth_token_current(Auth_token *token, char **hvalp, char **moving_factorp)
{
  char *hval, *mf;

  if (token_mode(token->mode, TOKEN_MODE_TIME)) {
	time_t now;

	time(&now);
	hval = auth_totp_value(token->digest_name, token->key, token->keylen,
						   &now, token->time_step, 0,
						   token->ndigits, token->base, &mf);
  }
  else {
	token_inc_counter(token->counter);
	mf = token_format_counter(token->counter);
	if (auth_token_set(token) == -1)
	  hval = NULL;
	else
	  hval = auth_hotp_value(token->key, token->keylen, token->counter,
							 TOKEN_HOTP_COUNTER_LENGTH,
							 token->ndigits, token->base);
  }

  if (hval == NULL)
	return(-1);

  if (hvalp != NULL)
	*hvalp = hval;
  if (moving_factorp != NULL)
	*moving_factorp = mf;

  return(0);
}

static int
auth_token_get_current(char *item_type, char *username, char **hvalp,
					   char **mfp, char **errmsg)
{
  int st;
  Auth_token *token;

  if ((token = auth_token_get(item_type, username)) == NULL) {
	*errmsg = ds_xprintf("Cannot find username: \"%s\"", username);
	return(-1);
  }

  if ((st = auth_token_current(token, hvalp, mfp)) == -1) {
	*errmsg = ds_xprintf("Could not get current OTP for \"%s\"",
						 token->username);
	return(-1);
  }

  return(0);
}

/*
 * Compute the HOTP value, given a KEY of length KLEN and a VALUE of length
 * VLEN.  Format the value into NDIGITS in radix BASE (10 or 32).
 * If BASE is 10, NDIGITS can be 6, 7, 8, or 9; if it is 32, NDIGITS must be 6.
 * RFC 4226 conformant.
 */
char *
auth_hotp_value(unsigned char *key, unsigned int klen, unsigned char *value,
				unsigned int vlen, unsigned int ndigits, unsigned int base)
{
  char *hval;

  hval = auth_token("SHA1", key, klen, value, vlen, ndigits, base);

  return(hval);
}

/*
 * Compute the TOTP value, using digest algorithm DIGEST_NAME
 * (which must be usable with HMAC), a KEY of length KLEN, a time
 * (CLOCK_VAL), a drift adjustment (DRIFT_ADJUST, in seconds) relative to that
 * time, and a window size (TIME_STEP).
 * If CLOCK_VAL is NULL, use the current time, and if TIME_STEP is zero, use
 * the algorithm's default (TOKEN_TOTP_DEFAULT_TIME_STEP_SECS).
 * Adjust the computed window by the window offset (which may be negative),
 * which is used to account for unsynchronized clocks.
 * Format the value into NDIGITS in radix BASE (10 or 32).
 * If BASE is 10, NDIGITS can be 6, 7, 8, or 9; if it is 32, NDIGITS must be 6.
 *
 * XXX This is based on the Internet-Draft and should therefore be considered
 * experimental.
 * XXX Maybe it's me, but the description of the algorithm in the
 * Internet-Draft seems poor by RFC standards; this implementation was
 * largely reverse engineered from the Internet-Draft's Reference
 * Implementation, which was not as helpful as it should be...
 */
char *
auth_totp_value(char *digest_name, unsigned char *key, unsigned int klen,
				time_t *clock_val, unsigned int time_step, int drift_adj,
				unsigned int ndigits, unsigned int base, char **mfp)
{
  unsigned int slen, ts;
  char *hval, *mf;
  time_t cv, t;
  unsigned char *v;

  /* Determine the base time. */
  if (clock_val == NULL)
	cv = time(NULL);
  else
	cv = *clock_val;

  /* Adjust the time, to compensate for relative clock drift. */
  cv += drift_adj;

  /* Determine the time step (window size, in seconds) to use. */
  if ((ts = time_step) == 0)
	ts = TOKEN_TOTP_DEFAULT_TIME_STEP_SECS;

  /* Compute the resulting window. */
  t = cv / ts;

  mf = ds_xprintf("%.16lx", t);
  if ((v = strhextob(mf, &slen)) == NULL)
	return(NULL);

  hval = auth_token(digest_name, key, klen, v, slen, ndigits, base);

  if (mfp != NULL)
	*mfp = mf;

  return(hval);
}

/*
 * If the token has a PIN, validate it.
 * Return 1 if the PIN was validated, 0 if there is no PIN, or -1 if the
 * validation failed.
 * XXX not computing a hash may reveal that there is no PIN.
 */
static int
validate_pin(Auth_token *token, char *pin)
{
  int st;
  char *digest_name;
  Passwd_digest_alg alg;

  if ((token->pin_hash != NULL && (pin == NULL || *pin == '\0'))
	  || (token->pin_hash == NULL && (pin != NULL && *pin != '\0'))) {
	log_msg((LOG_ERROR_LEVEL, "Missing or unexpected PIN"));
	return(-1);
  }

  if (pin != NULL && *pin != '\0') {
	Pw_entry *pw;

	digest_name = NULL;
	if (passwd_get_digest_algorithm(&digest_name, &alg) == -1) {
	  log_msg((LOG_ERROR_LEVEL,
			   "An unrecognized digest method is configured"));
	  return(-1);
	}

	if ((pw = pw_parse_entry(NULL, token->pin_hash)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Error parsing digest argument"));
	  return(-1);
	}

	if ((st = passwd_check_digest(alg, pin, pw)) == 1)
	  log_msg((LOG_TRACE_LEVEL, "PIN is correct"));
	else {
	  st = -1;
	  log_msg((LOG_TRACE_LEVEL, "PIN is incorrect"));
	}
  }
  else {
	st = 0;
	log_msg((LOG_TRACE_LEVEL, "Note: account has no PIN"));
  }

  return(st);
}

static int
totp_sync(char *otp_value, char *digest_name, unsigned char *key,
		  unsigned int keylen, unsigned int time_step, unsigned int ndigits,
		  unsigned int base, int *offset)
{
  int rc, window;
  char *hval;
  time_t now;

  time(&now);

  /* Check for the expected value first. */
  hval = auth_totp_value(digest_name, key, keylen,
						 &now, time_step, 0, ndigits, base, NULL);

  if (streq(otp_value, hval)) {
	*offset = 0;
	return(0);
  }

  rc = -1;
  window = 0;
  while (window++ < TOKEN_TOTP_MAX_SYNC) {
	hval = auth_totp_value(digest_name, key, keylen, &now, time_step,
						   window * time_step, ndigits, base, NULL);
	if (streq(otp_value, hval)) {
	  *offset = window * time_step;
	  rc = 1;
	  break;
	}

	hval = auth_totp_value(digest_name, key, keylen, &now, time_step,
						   -window * time_step, ndigits, base, NULL);
	if (streq(otp_value, hval)) {
	  *offset = -window * time_step;
	  rc = 1;
	  break;
	}

	if (isatty(0))
	  printf("[%d secs]\r", window * time_step);
  }

  if (isatty(0))
	printf("[%d secs]\n", window * time_step);

  return(rc);
}

/*
 * Test if OTP_VALUE is the expected one-time password given the TOTP token
 * parameters.
 */
static int
totp_validate(char *otp_value, char *digest_name, unsigned char *key,
			  unsigned int keylen, unsigned int time_step,
			  int clock_delta, unsigned int drift_window, unsigned int ndigits,
			  unsigned int base, int *driftp)
{
  int i;
  char *hval;
  time_t now;

  time(&now);

  /* Adjust for clock skew. */
  now += clock_delta;

  /* Check for the expected value first. */
  hval = auth_totp_value(digest_name, key, keylen,
						 &now, time_step, 0, ndigits, base, NULL);

  if (streq(otp_value, hval)) {
	if (driftp != NULL)
	  *driftp = 0;
	log_msg((LOG_TRACE_LEVEL, "Exact TOTP match"));
	return(0);
  }

  for (i = 1; i <= drift_window; i++) {
	hval = auth_totp_value(digest_name, key, keylen, &now, time_step,
						   i * time_step, ndigits, base, NULL);
	if (streq(otp_value, hval)) {
	  if (driftp != NULL)
		*driftp = i;
	  log_msg((LOG_TRACE_LEVEL, "TOTP matched in +%d window", i));
	  return(0);
	}
	hval = auth_totp_value(digest_name, key, keylen, &now, time_step,
						   -i * time_step, ndigits, base, NULL);
	if (streq(otp_value, hval)) {
	  if (driftp != NULL)
		*driftp = -i;
	  log_msg((LOG_TRACE_LEVEL, "TOTP matched in -%d window", i));
	  return(0);
	}
  }

  log_msg((LOG_DEBUG_LEVEL, "TOTP failed to match within %d windows",
		   drift_window));
  return(-1);
}

/*
 * For the given TOTP, verify that the current value is OTP_VALUE,
 * within a window of plus-or-minus DRIFT_WINDOW intervals, each of
 * size TIME_STEP seconds.
 * If successful and DRIFTP is non-NULL, set it to the relative interval in
 * which OTP_VALUE was found; this allows the caller to adjust for clock
 * skew in future validation calls.
 *
 * Return 0 if successful, -1 otherwise.
 * XXX the window could be asymmetrical
 */
int
auth_totp_token_validate(Auth_token *token, char *otp_value, char *pin,
						 Token_pin_mode pin_mode, unsigned int time_step,
						 int clock_delta, unsigned int drift_window,
						 unsigned int ndigits, unsigned int base, int *driftp)
{

  if (token->mode & TOKEN_MODE_DISABLED) {
	log_msg((LOG_ERROR_LEVEL, "Token is disabled"));
	return(-1);
  }

  if ((pin == NULL && pin_mode != TOKEN_IGNORABLE_PIN)
	  || validate_pin(token, pin) == -1) {
	log_msg((LOG_ERROR_LEVEL, "PIN validation failed"));
	return(-1);
  }

  if (totp_validate(otp_value, token->digest_name, token->key,
					token->keylen, time_step, clock_delta,
					drift_window, ndigits, base, driftp) == -1)
	return(-1);

  return(0);
}

typedef struct Totp_test {
  char *secret;
  time_t tval;
  unsigned int time_step;
  char *digest_name;
  char *value;
} Totp_test;

/* From draft-mraihi-totp-timebased-05.txt (March 8, 2010), Appendex B */
static Totp_test totp_ietfdraft_test[] = {
  { "3132333435363738393031323334353637383930", 59, 0,
	"SHA1",   "94287082" },
  { "3132333435363738393031323334353637383930", 59, 0,
	"SHA256", "32247374" },
  { "3132333435363738393031323334353637383930", 59, 0,
	"SHA512", "69342147" },
  { "3132333435363738393031323334353637383930", 1111111109, 0,
	"SHA1", "07081804" },
  { "3132333435363738393031323334353637383930", 1111111109, 0,
	"SHA256", "34756375"},
  { "3132333435363738393031323334353637383930", 1111111109, 0,
	"SHA512", "63049338" },
  { "3132333435363738393031323334353637383930", 1111111111, 0,
	"SHA1", "14050471" },
  { "3132333435363738393031323334353637383930", 1111111111, 0,
	"SHA256", "74584430" },
  { "3132333435363738393031323334353637383930", 1111111111, 0,
	"SHA512", "54380122" },
  { "3132333435363738393031323334353637383930", 1234567890, 0,
	"SHA1", "89005924" },
  { "3132333435363738393031323334353637383930", 1234567890, 0,
	"SHA256", "42829826" },
  { "3132333435363738393031323334353637383930", 1234567890, 0,
	"SHA512", "76671578" },
  { "3132333435363738393031323334353637383930", 2000000000, 0,
	"SHA1", "69279037" },
  { "3132333435363738393031323334353637383930", 2000000000, 0,
	"SHA256", "78428693" },
  { "3132333435363738393031323334353637383930", 2000000000, 0,
	"SHA512", "56464532" },
  { NULL, 0, 0, NULL, NULL }
};

typedef struct Hotp_test {
  char *secret;
  int counter_start;
  char *value[20];
} Hotp_test;

/* See RFC 4226, Appendix D, p. 32 */
static Hotp_test hotp_rfc_test = {
  "12345678901234567890", 0,
  { "755224",		/* Count==0 */
	"287082",		/* Count==1 */
	"359152",		/* Count==2 */
	"969429",		/* Count==3 */
	"338314",		/* Count==4 */
	"254676",		/* Count==5 */
	"287922",		/* Count==6 */
	"162583",		/* Count==7 */
	"399871",		/* Count==8 */
	"520489",		/* Count==9 */
	NULL
  }
};

/* Authenex test unit 025 */
static Hotp_test hotp_unit025_test = {
  "78b80dcd4bcad98125ac59f45ca39ca2f4d3df9a", 4,
  {
	"468551",		/* Count==4 */
	"342073",		/* Count==5 */
	"691679",		/* Count==6 */
	"752000",		/* Count==7 */
	NULL
  }
};

static Hotp_test hotp_unit026_test = {
  "e09b14a35275216fbb2ec093941d3a60af0b13f3", 11,
  {
	"481924",		/* Count==11 */
	"013569",		/* Count==12 */
	"374126",		/* Count==13 */
	"804632",		/* Count==14 */
	NULL
  }
};

/*
 * See RFC 4226, Appendix D, p. 32
 */
static int
token_test(void)
{
  int i, start;
  unsigned int slen;
  char *hval, *secret;
  unsigned char *counter, *usecret;

  fprintf(stderr, "TOTP test vectors...\n");
  for (i = 0; totp_ietfdraft_test[i].secret != NULL; i++) {
	if ((usecret = strhextob(totp_ietfdraft_test[i].secret, &slen)) == NULL)
	  return(-1);
	hval = auth_totp_value(totp_ietfdraft_test[i].digest_name, usecret, slen,
						   &totp_ietfdraft_test[i].tval, 0,
						   totp_ietfdraft_test[i].time_step, 8, 10, NULL);
	if (streq(hval, totp_ietfdraft_test[i].value))
	  fprintf(stderr, "%3d. %s (ok)\n", i + 1, hval);
	else {
	  fprintf(stderr, "%3d. %s (error, expected: %s)\n",
			  i + 1, hval, totp_ietfdraft_test[i].value);
	  return(-1);
	}
  }

  fprintf(stderr, "\nHOTP test vectors...\n");
  secret = hotp_rfc_test.secret;
  start = hotp_rfc_test.counter_start;
  counter = token_set_counter(NULL, start);
  fprintf(stderr, "RFC 4226:\n");
  for (i = 0; hotp_rfc_test.value[i] != NULL; i++) {
	fprintf(stderr, "%3d. ", i + start);
	hval = auth_hotp_value((unsigned char *) secret, strlen(secret), counter,
						   TOKEN_HOTP_COUNTER_LENGTH,
						   TOKEN_HOTP_NDIGITS, TOKEN_HOTP_BASE);
	if (streq(hval, hotp_rfc_test.value[i]))
	  fprintf(stderr, " %s (ok)\n", hval);
	else {
	  fprintf(stderr, " %s (error, expected: %s)\n",
			  hval, hotp_rfc_test.value[i]);
	  return(-1);
	}
	token_inc_counter(counter);
  }

  if ((usecret = strhextob(hotp_unit025_test.secret, &slen)) == NULL)
	return(-1);
  start = hotp_unit025_test.counter_start;
  counter = token_set_counter(NULL, start);
  fprintf(stderr, "Unit025:\n");
  for (i = 0; hotp_unit025_test.value[i] != NULL; i++) {
	fprintf(stderr, "%3d. ", i + start);
	hval = auth_hotp_value(usecret, slen, counter, TOKEN_HOTP_COUNTER_LENGTH,
						   TOKEN_HOTP_NDIGITS, TOKEN_HOTP_BASE);
	if (streq(hval, hotp_unit025_test.value[i]))
	  fprintf(stderr, " %s (ok)\n", hval);
	else {
	  fprintf(stderr, " %s (error, expected: %s)\n",
			  hval, hotp_unit025_test.value[i]);
	  return(-1);
	}
	token_inc_counter(counter);
  }

  if ((usecret = strhextob(hotp_unit026_test.secret, &slen)) == NULL)
	return(-1);
  start = hotp_unit026_test.counter_start;
  counter = token_set_counter(NULL, start);
  fprintf(stderr, "Unit026:\n");
  for (i = 0; hotp_unit026_test.value[i] != NULL; i++) {
	fprintf(stderr, "%3d. ", i + start);
	hval = auth_hotp_value(usecret, slen, counter, TOKEN_HOTP_COUNTER_LENGTH,
						   TOKEN_HOTP_NDIGITS, TOKEN_HOTP_BASE);
	if (streq(hval, hotp_unit026_test.value[i]))
	  fprintf(stderr, " %s (ok)\n", hval);
	else {
	  fprintf(stderr, " %s (error, expected: %s)\n",
			  hval, hotp_unit026_test.value[i]);
	  return(-1);
	}
	token_inc_counter(counter);
  }

  fprintf(stderr, "All HOTP tests succeeded!\n");

  return(0);
}

static void
totp_now(Auth_token *token, unsigned char *user_secret, unsigned int user_slen,
		 unsigned int show_count, char *user_digest_name,
		 unsigned int time_step, unsigned int ndigits, unsigned int base)
{
  int i;
  char *digest_name, *hval, *prompt;
  unsigned int slen;
  unsigned char *secret;
  time_t now;
  Ds *ds;

  if (token != NULL) {
	int offset;

	if (user_digest_name != NULL)
	  digest_name = user_digest_name;
	else
	  digest_name = token->digest_name;

	time(&now);
	offset = -(time_step * show_count);
	for (i = 0; i < (show_count * 2 + 1); i++) {
	  hval = auth_totp_value(digest_name, token->key, token->keylen,
							 &now, time_step, offset, ndigits, base, NULL);
	  printf("TOTP=%s [at t=%ld, drift=%+5dsecs, interval=%+3d]\n",
			 hval, now + offset, offset, offset / (int) time_step);
	  offset += time_step;
	}

	return;
  }
  
  ds = ds_init(NULL);

  if (user_slen) {
	slen = user_slen;
	secret = user_secret;
  }
  else {
	char *secret_str;

	if ((secret_str = ds_prompt(ds, "Secret key string? ", 0)) == NULL
		|| *secret_str == '\0')
	  return;

	if ((secret = strhextob(secret_str, &slen)) == NULL) {
	  fprintf(stderr, "Invalid string\n");
	  return;
	}
	strzap(secret_str);
  }

  if (user_digest_name != NULL)
	digest_name = user_digest_name;
  else
	digest_name = "SHA1";

  ds_reset(ds);
  prompt = "Type Return for current value [EOF to quit]: ";

  for (i = 0; i < show_count; i++) {
	char *line;

#ifdef HAVE_READLINE
	line = ds_readline(ds, prompt, NULL);
#else
	printf("%s", prompt);
	fflush(stdout);
	line = ds_gets(ds, stdin);
#endif
	if (line == NULL) {
	  printf("\n");
	  break;
	}

	time(&now);
	hval = auth_totp_value(digest_name, secret, slen, &now,
						   time_step, 0, ndigits, base, NULL);
	printf("TOTP=%s [at t=%ld]\n", hval, now);
  }

}

static Auth_token *
auth_token_new(Auth_token *token, Auth_token_param *param)
{
  Auth_token *h;

  if ((h = token) == NULL)
	h = ALLOC(Auth_token);

  h->username = NULL;
  h->item_type = NULL;
  if (token_mode(param->mode, TOKEN_MODE_COUNTER)) {
	if (param->digest_name == NULL)
	  h->digest_name = "SHA1";
	else
	  h->digest_name = strdup(param->digest_name);
  }
  else if (token_mode(param->mode, TOKEN_MODE_TIME)) {
	if (param->digest_name == NULL)
	  h->digest_name = "SHA1";
	else
	  h->digest_name = strdup(param->digest_name);
  }
  else {
	log_msg((LOG_ERROR_LEVEL, "Invalid token mode"));
	return(NULL);
  }

  h->ndigits = param->ndigits;
  h->base = param->base;
  h->mode = param->mode;
  h->serial = NULL;
  h->counter = NULL;
  h->key = NULL;
  h->keylen = 0;
  h->drift = 0;
  h->pin_hash = NULL;
  time(&h->last_update);

  return(h);
}

static Auth_token *
auth_token_init(Auth_token_param *tp)
{
  char *cstr;
  Auth_token *token;

  if (token_mode(tp->mode, TOKEN_MODE_COUNTER)) {
	token = auth_token_new(NULL, tp);

	if ((cstr = canonicalize_counter(tp->device.hotp->counter_str)) == NULL)
	  return(NULL);

	if ((token->counter = strhextob(cstr, NULL)) == NULL)
	  return(NULL);
  }
  else {
	token = auth_token_new(NULL, tp);
	token->time_step = tp->device.totp->time_step;
  }

  token->item_type = strdup(tp->item_type);

  if (tp->username != NULL)
	token->username = strdup(tp->username);

  if (tp->serial != NULL)
	token->serial = strdup(tp->serial);

  if ((token->key = strhextob(tp->key_str, &token->keylen)) == NULL)
	return(NULL);

  if (tp->pin != NULL) {
	if ((token->pin_hash = make_pin_hash(tp->pin)) == NULL)
	  return(NULL);
  }

  return(token);
}

static int
list_compar_username(const void *a, const void *b)
{
  char **p, **q;

  p = (char **) a;
  q = (char **) b;

  return(strcmp(*p, *q));
}

static int
list_add(char *naming_context, char *name, void ***names)
{
  Dsvec *dsv;

  dsv = (Dsvec *) names;
  dsvec_add_ptr(dsv, strdup(name));

  return(1);
}

static void
list_qsort(void *base, size_t nmemb, size_t size,
		   int (*compar)(const void *, const void *))
{
  void *b;
  Dsvec *dsv;

  dsv = (Dsvec *) base;
  b = (void *) dsvec_base(dsv);

  qsort(b, nmemb, size, compar);
}

/*
 * Return a vector of token owners.
 */
Dsvec *
auth_token_list(char *item_type)
{
  int n;
  Dsvec *dsv;
  Vfs_handle *h;
  
  if ((h = vfs_open_item_type(item_type)) == NULL) {
	return(NULL);
  }

  dsv = dsvec_init(NULL, sizeof(char *));
  h->list_sort = list_qsort;
  n = vfs_list(h, NULL, list_compar_username, list_add, (void ***) dsv);
  if (n == -1)
    return(NULL);

  vfs_close(h);

  return(dsv);
}

char *
auth_token_get_flattened(char *item_type, char *username)
{
  char *token_str;
  Vfs_handle *h;
  
  if ((h = vfs_open_item_type(item_type)) == NULL) {
	return(NULL);
  }

  if (vfs_get(h, username, (void **) &token_str, NULL) == -1) {
	vfs_close(h);
	return(NULL);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_close() failed"));

  return(token_str);
}

Auth_token *
auth_token_get(char *item_type, char *username)
{
  char *token_str;
  Auth_token *token;
  
  if ((token_str = auth_token_get_flattened(item_type, username)) == NULL)
	return(NULL);

  if ((token = auth_token_unflatten(token_str, item_type)) == NULL)
	return(NULL);

  return(token);
}

int
auth_token_delete(char *item_type, char *username)
{
  Vfs_handle *h;

  if ((h = vfs_open_item_type(item_type)) == NULL) {
	return(-1);
  }

  if (vfs_delete(h, username) == -1) {
	vfs_close(h);
	return(-1);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_close() failed"));

  return(0);
}

/*
 * Return a list of all tokens in ITEM_TYPE, or NULL if an error occurs.
 */
Dsvec *
auth_token_get_all(char *item_type)
{
  int i;
  Auth_token *token;
  Dsvec *dsv, *tokens;

  if ((dsv = auth_token_list(item_type)) == NULL)
	return(NULL);

  tokens = dsvec_init(NULL, sizeof(Auth_token));
  for (i = 0; i < dsvec_len(dsv); i++) {
	char *u;

	u = (char *) dsvec_ptr_index(dsv, i);
	if ((token = auth_token_get(item_type, u)) == NULL)
	  return(NULL);
	dsvec_add_ptr(tokens, token);
  }

  return(tokens);
}

static char *
add_vfs(char *vfs_uri)
{
  char *uri;
  Kwv *kwv_conf;
  Vfs_directive *vd;

  if ((vd = vfs_uri_parse(vfs_uri, NULL)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Invalid vfs_uri: \"%s\"", vfs_uri));
	return(NULL);
  }

  if (vd->item_type == NULL) {
	time_t now;

	/* This is a bit dodgey... */
	time(&now);
	uri = ds_xprintf("[auth_tokens_%lu]%s", (unsigned long) now, vfs_uri);
	if ((vd = vfs_uri_parse(uri, NULL)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Invalid vfs_uri: \"%s\"", vfs_uri));
	  return(NULL);
	}
  }
  else
	uri = strdup(vfs_uri);

  if (dacs_conf == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Initialization error?!"));
	return(NULL);
  }

  kwv_conf = var_ns_lookup_kwv(dacs_conf->conf_var_ns, "Conf");
  if (kwv_conf == NULL) {
	kwv_conf = kwv_init(4);
	dacs_conf->conf_var_ns = var_ns_create("Conf", kwv_conf);
  }

  add_vfs_uri(kwv_conf, uri);

  return(strdup(vd->item_type));
}

static Auth_token *
lookup_by_serial(char *serial)
{
  int i;
  Auth_token *token;
  Dsvec *dsv;

  if ((dsv = auth_token_get_all(auth_token_item_type)) == NULL)
	return(NULL);

  for (i = 0; i < dsvec_len(dsv); i++) {
	token = (Auth_token *) dsvec_ptr_index(dsv, i);
	if (streq(serial, token->serial))
	  return(token);
  }

  return(NULL);
}

/*
 * Count the number of accounts with a serial number string of SERIAL,
 * or -1 if an error occurs.
 */
static int
serial_count(char *serial)
{
  int count, i;
  Auth_token *token;
  Dsvec *dsv;

  if ((dsv = auth_token_get_all(auth_token_item_type)) == NULL)
	return(0);

  count = 0;
  for (i = 0; i < dsvec_len(dsv); i++) {
	token = (Auth_token *) dsvec_ptr_index(dsv, i);
	if (streq(serial, token->serial))
	  count++;
  }

  return(count);
}

/*
 * Given a sequence SEQ of successive OTP values, try to determine what the
 * current counter value should be.  This involves an exhaustive search using
 * increasing counter values.  Since counters generally appear to be
 * initialized to zero we are likely to find the sequence quite quickly if
 * it is correct.
 * If COUNTER is provided, start there instead of at zero.
 *
 * If successful, the counter is left so that when it is incremented it will
 * produce the next OTP.
 * The caller is responsible for updating the database entry.
 *
 * This function is used to synchronize the server's counter with the counter
 * in the OTP token.  It will probably be used when a token is first deployed
 * and when a token appears to stop working because OTPs have been emitted
 * by the token but not submitted to the server.
 *
 * Return 0 if successful, -1 if no resynch fails.
 */
static int
hotp_sync(Auth_token *token, Dsvec *dsv_seq, unsigned char *counter)
{
  int h, i;
  char *hval, **hbufs;
  Dsvec *dsv_hval;

  log_msg((LOG_TRACE_LEVEL, "Syncing token for \"%s\"", token->username));
  if (dsvec_len(dsv_seq) != TOKEN_HOTP_SYNC_OTPS) {
	log_msg((LOG_ERROR_LEVEL, "Exactly %d successive OTPs are required",
			 TOKEN_HOTP_SYNC_OTPS));
	return(-1);
  }

  for (i = 0; i < TOKEN_HOTP_SYNC_OTPS; i++) {
	char *sync_val;

	sync_val = (char *) dsvec_ptr_index(dsv_seq, i);
	/*
	 * This doesn't check very carefully if the given values could not
	 * possibly match the computed ones (because of the base or number
	 * of digits).
	 * This is a feature, not a bug.
	 */
	if (!is_strclass(sync_val, STRCLASS_ALNUM)) {
	  log_msg((LOG_ERROR_LEVEL, "Invalid OTP value: \"%s\"", sync_val));
	  return(-1);
	}
  }
  
  if (counter == NULL)
	token_set_counter(token->counter, 0);
  else
	token->counter = counter;

  dsv_hval = dsvec_init(NULL, sizeof(char *));

  /* We only need to keep the most recent values to match. */
  hbufs = ALLOC_N(char *, TOKEN_HOTP_SYNC_OTPS);
  
  /* Prime the pump... */
  for (i = 0; i < TOKEN_HOTP_SYNC_OTPS; i++) {
	if (i != 0)
	  token_inc_counter(token->counter);

	hval = auth_hotp_value(token->key, token->keylen, token->counter,
						   TOKEN_HOTP_COUNTER_LENGTH,
						   token->ndigits, token->base);
	hbufs[i] = (char *) malloc(TOKEN_MAX_NDIGITS + 1);
	strcpy(hbufs[i], hval);
	log_msg((LOG_TRACE_LEVEL, "Seqnum%d: hval=\"%s\", counter=%s",
			 i, hval, token_format_counter(token->counter)));
	dsvec_add_ptr(dsv_hval, hbufs[i]);
  }

  /*
   * Compare the specified sequence against the computed one.
   * If they match, we're done; leave the counter so that when it is
   * incremented we will produce the next OTP in the sequence.  If they do not
   * match, discard the oldest value and compute the next one in the sequence.
   * We want to limit the search space to prevent denial of service attacks
   * if this is called by a web service.
   */
  h = 0;
  while (i < TOKEN_HOTP_MAX_SYNC) {
	if (dsvec_streq(dsv_hval, dsv_seq)) {
	  log_msg((LOG_DEBUG_LEVEL, "Sync succeeded"));
	  log_msg((LOG_TRACE_LEVEL, "hval=\"%s\", counter=%s",
			   hval, token_format_counter(token->counter)));
	  return(0);
	}

	token_inc_counter(token->counter);
	hval = auth_hotp_value(token->key, token->keylen, token->counter,
						   TOKEN_HOTP_COUNTER_LENGTH,
						   token->ndigits, token->base);
	dsvec_shift(dsv_hval, 1);

	strcpy(hbufs[h], hval);
	dsvec_add_ptr(dsv_hval, hbufs[h]);
	h = (h + 1) % TOKEN_HOTP_SYNC_OTPS;
	i++;
  }

  log_msg((LOG_ERROR_LEVEL, "Could not resynchronize after %d values", i));

  return(-1);
}

static int
token_sync(Auth_token *token, Dsvec *dsv_sync, char **errmsg)
{
  int st;

  if (token_mode(token->mode, TOKEN_MODE_TIME)) {
	int diff, nintervals, offset;
	char *given_otp_value;

	given_otp_value = (char *) dsvec_ptr_index(dsv_sync, 0);
	st = totp_sync(given_otp_value, token->digest_name,
				   token->key, token->keylen,
				   token->time_step, token->ndigits, token->base, &offset);
	if (st == -1) {
	  *errmsg = "Could not synchronize TOTP token";
	  return(-1);
	}

	if (st == 0)
	  log_msg((LOG_DEBUG_LEVEL, "Token is synchronized"));
	else {
	  diff = (offset < 0) ? -offset : offset;
	  nintervals = diff / token->time_step;
	  log_msg((LOG_DEBUG_LEVEL, "Token's clock is %s by about %d seconds",
			   (offset < 0) ? "slow" : "fast", diff));
	  log_msg((LOG_DEBUG_LEVEL, "Delta is %d %s at interval size %d secs",
			   nintervals, (nintervals == 1) ? "interval" : "intervals",
			   token->time_step));
	  token->drift = offset;
	}
  }
  else {
	if (hotp_sync(token, dsv_sync, NULL) == -1) {
	  *errmsg = "Could not synchronize HOTP token";
	  return(-1);
	}
  }

  if ((st = auth_token_set(token)) == -1)
	*errmsg = "Could not set token";

  return(st);
}

static int
auth_token_sync(char *item_type, char *username, Dsvec *dsv_sync, char *pin,
				char **errmsg)
{
  Auth_token *token;

  if ((token = auth_token_get(item_type, username)) == NULL) {
	*errmsg = ds_xprintf("Cannot find username: \"%s\"", username);
	return(-1);
  }

  if (token_sync(token, dsv_sync, errmsg) == -1)
	return(-1);

  return(0);
}

/*
 * Search for OTP_VALUE, within a window of WINDOW counter values
 * *after* the expected counter value of COUNTER (CLEN bytes long), using KEY
 * (KLEN bytes long), NDIGITS, and BASE.
 * If OTP_VALUE is found, return 0 and leave COUNTER at the correct value;
 * otherwise, return -1.
 *
 * A hardware token's counter is a monotonically increasing value, so it should
 * never be "behind" the server's - and a counter value should never be reused
 * with the same key value.  But a software client might have its counter set
 * to an arbitrary value, or perhaps an administrative error (or a bug) might
 * incorrectly advance the server's counter ahead.
 * In these cases, the synchronization operation must be performed; if this
 * function were allowed to scan "backwards", then a one-time password could
 * be reused.
 */
static int
hotp_validate(char *otp_value, unsigned char *key, unsigned int klen,
			  unsigned char *counter, unsigned int clen, unsigned int ndigits,
			  unsigned int base, unsigned int window)
{
  int i;
  char *hval;
  unsigned char *saved_counter;

  /*
   * Validate the one-time password.
   * Check for the expected value first, then search near the expected
   * value if allowable.
   */
  saved_counter = token_copy_counter(counter, NULL);
  token_inc_counter(counter);

  log_msg((LOG_TRACE_LEVEL, "Checking for expected HOTP match"));
  hval = auth_hotp_value(key, klen, counter, clen, ndigits, base);
  if (streq(otp_value, hval)) {
	log_msg((LOG_DEBUG_LEVEL, "HOTP matched, counter=%s",
			 token_format_counter(counter)));
	return(0);
  }
  log_msg((LOG_TRACE_LEVEL, "HOTP=\"%s\", counter=%s",
		   hval, token_format_counter(counter)));

  /*
   * Look forward through the counter space within the window limit.
   */
  log_msg((LOG_TRACE_LEVEL, "Scanning forward for HOTP match"));
  for (i = 1; i <= window; i++) {
	token_inc_counter(counter);
	hval = auth_hotp_value(key, klen, counter, clen, ndigits, base);
	if (streq(otp_value, hval)) {
	  log_msg((LOG_DEBUG_LEVEL, "HOTP matched, counter=%s",
			   token_format_counter(counter)));
	  return(0);
	}
	log_msg((LOG_TRACE_LEVEL, "HOTP=\"%s\", counter=%s",
			 hval, token_format_counter(counter)));
  }

  log_msg((LOG_DEBUG_LEVEL, "HOTP did not match"));
  return(-1);
}

/*
 * For the given HOTP token, verify that the next value is OTP_VALUE,
 * within a window of plus-or-minus WINDOW values.
 * Note that the counter is *not* advanced - the caller must increment and save
 * the counter, if necessary.
 *
 * Return 0 if successful, -1 otherwise.
 * XXX the window could be asymmetrical
 */
int
auth_hotp_token_validate(Auth_token *token, char *otp_value, char *pin,
						 Token_pin_mode pin_mode, unsigned int window)
{
  int st;

  if (token->mode & TOKEN_MODE_DISABLED) {
	log_msg((LOG_ERROR_LEVEL, "Token for \"%s\" is disabled", token->username));
	return(-1);
  }

  if (pin == NULL && pin_mode == TOKEN_IGNORABLE_PIN)
	log_msg((LOG_DEBUG_LEVEL, "Ignoring token PIN validation for \"%s\"",
			 token->username));
  else if (validate_pin(token, pin) == -1) {
	/* The PIN was invalid, do not proceed. */
	log_msg((LOG_ERROR_LEVEL, "Token PIN validation failed for \"%s\"",
			 token->username));
	return(-1);
  }

  /*
   * Check for a sync request after the PIN check.
   * We assume it is a sync request if the argument is too long to be
   * an OTP.
   */
  if (strlen(otp_value) > token->ndigits) {
	Dsvec *sync;

	log_msg((LOG_DEBUG_LEVEL, "This appears to be a sync request"));
	if ((sync = strsplit(otp_value, ",", 0)) == NULL
		|| dsvec_len(sync) != TOKEN_HOTP_SYNC_OTPS) {
	  log_msg((LOG_ERROR_LEVEL, "Invalid sync string syntax"));
	  return(-1);
	}

	if (hotp_sync(token, sync, NULL) == -1) {
	  log_msg((LOG_ERROR_LEVEL,
			   "Sync failed - please contact a system administrator"));
	  return(-1);
	}

	log_msg((LOG_DEBUG_LEVEL, "HOTP matched"));
	return(0);
  }

  st = hotp_validate(otp_value, token->key, token->keylen, token->counter,
					 TOKEN_HOTP_COUNTER_LENGTH, token->ndigits, token->base,
					 window);

  return(st);
}

/*
 * This is the entry point from local_token_authenticate to validate an OTP
 * (of whichever type) and, optionally, a PIN associated with the account.
 * Return 0 if successful, -1 otherwise.
 */
int
auth_token_validate(Auth_token *token, char *otp_value, char *pin,
					Token_pin_mode pin_mode, char *challenge,
					unsigned int window)
{
  int drift, st;

  st = -1;
  if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	log_msg((LOG_TRACE_LEVEL, "Validating HOTP..."));
	st = auth_hotp_token_validate(token, otp_value, pin, pin_mode, window);
  }
  else if (token_mode(token->mode, TOKEN_MODE_TIME)) {
	log_msg((LOG_TRACE_LEVEL, "Validating TOTP..."));
	/* XXX the size of the drift window should be configurable */
	st = auth_totp_token_validate(token, otp_value, pin, pin_mode,
								  token->time_step, token->drift,
								  TOKEN_TOTP_DEFAULT_DRIFT_WINDOW,
								  token->ndigits, token->base, &drift);
  }
	
  return(st);
}

/*
 * Increment and store the HOTP token account's counter value.
 * This is generally called when a HOTP token supplies a correct
 * one-time password (e.g., during authentication).
 * It is a no-op for TOTP.
 */
int
auth_token_update(Auth_token *token)
{
  int st;

  if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	token_inc_counter(token->counter);
	if ((st = auth_token_set(token)) == -1)
	  log_msg((LOG_ERROR_LEVEL, "Update failed"));
	else
	  log_msg((LOG_DEBUG_LEVEL, "Incremented and stored counter"));
  }
  else
	st = 0;

  return(st);
}

/*
 * Display account information for TOKEN, excluding the key, which
 * should remain secret.
 */
static void
auth_token_show(FILE *fp, Auth_token *token)
{

  if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	fprintf(fp, "%s,hotp,%s", 
			token->username,
			(token->mode & TOKEN_MODE_DISABLED) ? "disabled" : "enabled");
	fprintf(fp, ",\"%s\",%s,%s,%s\n",
			token->serial, (token->counter == NULL)
			? "????" : token_format_counter(token->counter),
			(token->pin_hash == NULL) ? "-PIN" : "+PIN",
			make_short_local_date_string(localtime(&token->last_update)));
  }
  else {
	fprintf(fp, "%s,totp,%s", 
			token->username,
			(token->mode & TOKEN_MODE_DISABLED) ? "disabled" : "enabled");
	fprintf(fp, ",\"%s\",%d,%s,%s\n",
			token->serial, token->drift,
			(token->pin_hash == NULL) ? "-PIN" : "+PIN",
			make_short_local_date_string(localtime(&token->last_update)));
  }
}

/*
 * 
 */
static Auth_token *
import_token(Kwv *kwv, Auth_token_mode required_mode, int replace,
			 char *pin_constraints, char **err)
{
  int count, enabled, type;
  char *errmsg, *p, *username;
  Auth_token *token;
  Auth_token_mode mode;

  if ((username = DEC(kwv_lookup_value(kwv, "u"))) == NULL) {
	errmsg = "Missing username (u) attribute";
	goto fail;
  }
  if (!is_valid_username(username)) {
	errmsg = ds_xprintf("Invalid username: \"%s\"", username);
	goto fail;
  }

  if ((p = kwv_lookup_value(kwv, "d")) == NULL) {
	errmsg = "Missing device (d) attribute";
	goto fail;
  }
  if (streq(p, "c"))
	type = TOKEN_MODE_COUNTER;
  else if (streq(p, "t"))
	type = TOKEN_MODE_TIME;
  else {
	errmsg = "Unrecognized device type";
	goto fail;
  }
  if (required_mode != TOKEN_MODE_UNKNOWN
	  && !token_mode(required_mode, type)) {
	/* Ignore this one. */
	*err = NULL;
	return(NULL);
  }

  /* Enabled */
  if ((p = kwv_lookup_value(kwv, "en")) == NULL) {
	errmsg = "Missing enabled (en) attribute";
	goto fail;
  }
  if (streq(p, "0"))
	enabled = 0;
  else if (streq(p, "1"))
	enabled = 1;
  else {
	errmsg = "Unrecognized enabled state";
	goto fail;
  }
  mode = type;
  if (!enabled)
	mode |= TOKEN_MODE_DISABLED;

  if (!replace && auth_token_get(auth_token_item_type, username) != NULL) {
	errmsg = ds_xprintf("Account exists for \"%s\"; use -import-replace?",
						username);
	goto fail;
  }

  token = ALLOC(Auth_token);
  token->username = username;
  token->item_type = auth_token_item_type;

  if ((token->serial = DEC(kwv_lookup_value(kwv, "s"))) == NULL) {
	errmsg = "Missing serial (s) attribute";
	goto fail;
  }

  if ((count = serial_count(token->serial)) > 1
	  || (count == 1 && !replace)) {
	errmsg = "Duplicate serial (s) attribute";
	goto fail;
  }
  if (count == -1) {
	errmsg = "Error while checking serial (s) attribute";
	goto fail;
  }

  token->mode = mode;
  if ((token->digest_name = kwv_lookup_value(kwv, "dn")) == NULL)
	token->digest_name = token_mode(token->mode, TOKEN_MODE_COUNTER)
	  ? TOKEN_HOTP_DEFAULT_HASH : TOKEN_TOTP_DEFAULT_HASH;

  if ((p = kwv_lookup_value(kwv, "k")) == NULL) {
	errmsg = "Missing key (k) attribute";
	goto fail;
  }
  if (strlen(p) < TOKEN_MIN_KEY_LENGTH_BYTES) {
	errmsg = ds_xprintf("Key is too short, minimum is %d bytes",
						TOKEN_MIN_KEY_LENGTH_BYTES);
	goto fail;
  }
  if ((token->key = strhextob(p, &token->keylen)) == NULL) {
	errmsg = "Invalid hex key string";
	goto fail;
  }

  if ((p = kwv_lookup_value(kwv, "nd")) != NULL) {
	if (strnum(p, STRNUM_UI, &token->ndigits) == -1) {
	  errmsg = "Invalid ndigits (nd) value";
	  goto fail;
	}
  }
  else
	token->ndigits = token_mode(token->mode, TOKEN_MODE_COUNTER)
	  ? TOKEN_HOTP_NDIGITS : TOKEN_TOTP_NDIGITS;

  if ((p = kwv_lookup_value(kwv, "b")) != NULL) {
	if (strnum(p, STRNUM_UI, &token->base) == -1
		|| (token->base != 10 && token->base != 16 && token->base != 32)) {
	  errmsg = "Invalid base value";
	  goto fail;
	}
  }
  else
	token->base = token_mode(token->mode, TOKEN_MODE_COUNTER)
	  ? TOKEN_HOTP_BASE : TOKEN_TOTP_BASE;

  if (token_mode(token->mode, TOKEN_MODE_TIME)) {
	if ((p = kwv_lookup_value(kwv, "ts")) != NULL) {
	  if (strnum(p, STRNUM_UINZ, &token->time_step) == -1) {
		errmsg = "Invalid time step (ts) value";
		goto fail;
	  }
	}
	else
	  token->time_step = TOKEN_TOTP_DEFAULT_TIME_STEP_SECS;
  }
  else
	token->time_step = 0;

  if ((p = kwv_lookup_value(kwv, "dr")) != NULL) {
	if (strnum(p, STRNUM_I, &token->drift) == -1) {
	  errmsg = "Invalid drift (dr) value";
	  goto fail;
	}
  }
  else
	token->drift = 0;

  if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	if ((p = kwv_lookup_value(kwv, "c")) == NULL) {
	  errmsg = "Missing counter (c) attribute";
	  goto fail;
	}
	if ((token->counter = strhextob(canonicalize_counter(p), NULL)) == NULL) {
	  errmsg = "Invalid counter (c) value";
	  goto fail;
	}
  }
  else
	token->counter = NULL;

  if ((token->pin_hash = DEC(kwv_lookup_value(kwv, "ph"))) == NULL) {
	if ((p = kwv_lookup_value(kwv, "p")) != NULL) {
	  if (!pw_is_passwd_acceptable(p, pin_constraints)) {
		errmsg = "PIN does not meet constraints";
		goto fail;
	  }
	
	  if ((token->pin_hash = make_pin_hash(p)) == NULL) {
		errmsg = "Cannot make pin hash";
		goto fail;
	  }
	}
	else {
	  if (conf_val(CONF_TOKEN_REQUIRES_PIN) == NULL
		  || conf_val_eq(CONF_TOKEN_REQUIRES_PIN, "yes")) {
		errmsg = "This account must be created with a PIN";
		goto fail;
	  }
	  else if (!conf_val_eq(CONF_TOKEN_REQUIRES_PIN, "no")) {
		errmsg = "Invalid TOKEN_REQUIRES_PIN directive";
		goto fail;
	  }
	}
  }
  /* XXX there is no check to see if "ph" looks reasonable */

  if ((p = kwv_lookup_value(kwv, "lu")) != NULL) {
	if (strnum(p, STRNUM_TIME_T, &token->last_update) == -1) {
	  errmsg = "Invalid last update (lu) value";
	  goto fail;
	}
  }
  else
	time(&token->last_update);

  return(token);

 fail:
  *err = errmsg;
  return(NULL);
}

/*
 * Token import/export (provisioning)
 * Create accounts by reading <filename>, one account per line
 * It is ok to replace existing accounts with new data if REPLACE is non-zero.
 *
 * <!ELEMENT otp_token EMPTY>
 * <!ATTLIST otp_token
 *   username     CDATA   #REQUIRED			-- u
 *   device (c | t)       #REQUIRED			-- d
 *   serial       CDATA   #REQUIRED			-- s
 *   key          CDATA   #REQUIRED			-- k
 *   ndigits      CDATA   #REQUIRED         -- nd
 *   base         CDATA   #REQUIRED         -- b
 *   enabled (0 | 1)      #IMPLIED			-- en
 *   digest_name  CDATA   #IMPLIED			-- dn
 *   counter      CDATA   #IMPLIED			-- c
 *   pin          CDATA   #IMPLIED			-- p
 * >
 *
 * Also see Portable Symmetric Key Container (PSKC)
 * http://datatracker.ietf.org/doc/draft-ietf-keyprov-pskc/
 */
static int
auth_token_import(FILE *fp, Auth_token_mode mode, int replace,
				  char *pin_constraints)
{
  int linenum, saw_root;
  char *errmsg, *line, *startp;
  Auth_token *token;
  Ds *ds;
  Kwv *kwv;
  Kwv_xml *xml;

  ds = ds_init(NULL);
  dsio_set(ds, fp, NULL, 0, 0);
  ds->delnl_flag = 1;
  ds->crnl_flag = 1;

  linenum = 1;
  errmsg = NULL;
  saw_root = 0;

  while ((line = dsio_gets(ds)) != NULL) {
	startp = line;
	while (*startp == ' ' || *startp == '\t')
	  startp++;
	if (*startp == '\0' || *startp == '#') {
	  linenum++;
	  continue;
	}

	if ((xml = kwv_xml_parse(NULL, startp, &errmsg)) == NULL)
	  goto fail;
	if (xml->type == KWV_XML_COMMENT) {
	  linenum++;
	  continue;
	}

	if (!saw_root) {
	  if (!streq(xml->el_name, "otp_tokens")) {
		errmsg = "Invalid root element";
		goto fail;
	  }
	  if (xml->type == KWV_XML_EMPTY_TAG) {
		saw_root = 2;
		break;
	  }
	  if (xml->type != KWV_XML_START_TAG) {
		errmsg = "Invalid root tag";
		goto fail;
	  }
	  saw_root = 1;
	  linenum++;
	  continue;
	}

	if (xml->type == KWV_XML_END_TAG && streq(xml->el_name, "otp_tokens")) {
	  saw_root = 2;
	  break;
	}

	if (!streq(xml->el_name, "otp_token")) {
	  errmsg = "Invalid element";
	  goto fail;
	}

	kwv = xml->kwv_attrs;
	if ((token = import_token(kwv, mode, replace, pin_constraints,
							  &errmsg)) != NULL) {
	  if (auth_token_set(token) == -1) {
		errmsg = "Could not write account record";
		goto fail;
	  }
	}
	else if (errmsg != NULL)
	  goto fail;

	linenum++;
  }

  if (saw_root == 2)
	return(0);

  if (ferror(fp) || !feof(fp)) {
	errmsg = "Input error";
	goto fail;
  }

  errmsg = "XML parse error";

 fail:
  if (errmsg == NULL)
	fprintf(stderr, "Error at line %d: syntax error", linenum);
  else
	fprintf(stderr, "Line %d: %s", linenum, errmsg);
  if (line != NULL)
	fprintf(stderr, ": %s", line);
  fprintf(stderr, "\n");

  return(-1);
}

static Ds *
token_to_xml(Auth_token *token)
{
  Ds *ds;

  ds = ds_init(NULL);
  ds_asprintf(ds, "<otp_token");
  ds_asprintf(ds, " u='%s'", ENC(token->username));
  ds_asprintf(ds, " d='%s'",
			  token_mode(token->mode, TOKEN_MODE_COUNTER) ? "c" : "t");
  ds_asprintf(ds, " en='%s'", token_enabled(token->mode) ? "1" : "0");
  ds_asprintf(ds, " dn='%s'", token->digest_name);
  ds_asprintf(ds, " s='%s'", ENC(token->serial));
  ds_asprintf(ds, " k='%s'", strbenc64(token->key, token->keylen));
  ds_asprintf(ds, " nd='%u'", token->ndigits);
  ds_asprintf(ds, " b='%u'", token->base);
  if (token_mode(token->mode, TOKEN_MODE_TIME)) {
	if (token->time_step != 0)
	  ds_asprintf(ds, " ts='%u'", token->time_step);
	ds_asprintf(ds, " dr='%u'", token->drift);
  }
  else {
	if (token->counter != NULL)
	  ds_asprintf(ds, " c='%s'", token_format_counter(token->counter));
  }
  if (token->pin_hash != NULL)
	ds_asprintf(ds, " ph='%s'", ENC(token->pin_hash));
  ds_asprintf(ds, " lu='%u'", token->last_update);
  ds_asprintf(ds, "/>");

  return(ds);
}

/*
 * Emit an XML element to FP that represents TOKEN.
 */
static int
export_token(FILE *fp, Auth_token *token)
{
  Ds *ds;

  ds = token_to_xml(token);
  fprintf(fp, "%s\n", ds_buf(ds));

  return(0);
}

static int
auth_token_export(FILE *fp, Auth_token_mode required_mode, char *username)
{
  Auth_token *token;

  if (username != NULL) {
	if ((token = auth_token_get(auth_token_item_type, username)) == NULL) {
	  fprintf(stderr, "Could not get account data for \"%s\"\n", username);
	  return(-1);
	}

	if (required_mode == TOKEN_MODE_UNKNOWN
		|| token_mode(token->mode, required_mode)) {
	  fprintf(fp, "<otp_tokens>\n");
	  export_token(fp, token);
	  fprintf(fp, "</otp_tokens>\n");
	}
  }
  else {
	int did_root, i;
	Dsvec *dsv;

	did_root = 0;
	dsv = auth_token_list(auth_token_item_type);
	for (i = 0; i < dsvec_len(dsv); i++) {
	  char *u;

	  u = (char *) dsvec_ptr_index(dsv, i);
	  if ((token = auth_token_get(auth_token_item_type, u)) == NULL) {
		fprintf(stderr, "Could not get account data for \"%s\"\n", u);
		return(-1);
	  }

	  if (required_mode == TOKEN_MODE_UNKNOWN
		  || token_mode(token->mode, required_mode)) {
		if (!did_root) {
		  fprintf(fp, "<otp_tokens>\n");
		  did_root = 1;
		}
		export_token(fp, token);
	  }
	}

	if (did_root)
	  fprintf(fp, "</otp_tokens>\n");
  }

  return(0);
}

/*
 * Convert from old account format - deprecated
 * Import old account records from FP, then export to stdout.
 * Deprecated.
 */
static int
auth_token_convert(FILE *fp, Auth_token_mode required_mode)
{
  int linenum;
  char *errmsg, *line;
  Auth_token *token;
  Ds *ds;

  ds = ds_init(NULL);
  dsio_set(ds, fp, NULL, 0, 0);

  linenum = 1;
  errmsg = NULL;
  while ((line = dsio_gets(ds)) != NULL) {
	if ((token = auth_token_old_unflatten(line)) == NULL) {
	  errmsg = "Cannot import record";
	  goto fail;
	}

	if (required_mode == TOKEN_MODE_UNKNOWN
		|| token_mode(token->mode, required_mode)) {
	  if (export_token(stdout, token) == -1) {
		errmsg = "Export failed";
		goto fail;
	  }
	}

	linenum++;
  }

  if (ferror(fp) || !feof(fp)) {
	errmsg = "Input error";
	goto fail;
  }

  return(0);

 fail:
  if (errmsg == NULL)
	fprintf(stderr, "Error at line %d: syntax error", linenum);
  else
	fprintf(stderr, "Line %d: %s", linenum, errmsg);
  if (line != NULL)
	fprintf(stderr, ": %s", line);
  fprintf(stderr, "\n");

  return(-1);
}

static int
auth_token_demo_create(Auth_token_param *param)
{
  Auth_token *token;
  Ds *ds;

  if ((token = auth_token_init(param)) == NULL)
	return(-1);

  if (auth_token_set(token) == -1)
	return(-1);

  ds = token_to_xml(token);
  log_msg((LOG_TRACE_LEVEL, "Create demo account:\n%s", ds_buf(ds)));

  printf("<b>An account has been created for username <tt>%s</tt></b>:\n",
		 token->username);
  printf("<blockquote><tt>%s</tt></blockquote>\n",
		 xml_escape_cdata(ds_buf(ds)));

  printf("<p>You have %sassigned a PIN to this account.\n",
		 (token->pin_hash == NULL) ? "<b>not</b> " : "");

  printf("<p><b>To continue,</b> ");
  printf("<a href=\"javascript:history.back()\">return to the demo page</a>.\n");

  return(0);
}

static void
html_header(char *mesg)
{
  Html_header_conf *html_conf;

  html_conf = emit_html_header_conf(NULL);
  html_conf->no_cache = 1;
  html_conf->title
	= ds_xprintf("DACS One-Time Password Token Demonstration -- %s", mesg);

  emit_html_header(stdout, html_conf);
}

typedef enum {
  TOKEN_OP_CURRENT       = 0,
  TOKEN_OP_DEMO_CREATE   = 1,
  TOKEN_OP_DEMO_SYNC     = 2,
  TOKEN_OP_DEMO_VALIDATE = 3,
  TOKEN_OP_SET_PIN       = 4,
  TOKEN_OP_SYNC          = 5,
  TOKEN_OP_UNKNOWN       = -1
} Token_op;

/*
 * Web services for self-serve OTP token accounts
 */
static int
dacs_token(Kwv *kwv, char *remote_addr, char **errmsg)
{
  int admin, is_demo, st;
  unsigned int ncookies;
  char *confirm_new_pin, *mode_str, *new_pin, *operation, *user_ident;
  char *password, *pin, *sync_str, *username;
  Auth_token *token;
  Cookie *cookies;
  Credentials *credentials, *selected;
  Dsvec *dsv_sync;
  Token_op op;
  Auth_token_mode mode;

  is_demo = 0;
  operation = kwv_lookup_value(kwv, "OPERATION");
  if (operation == NULL) {
    *errmsg = "No OPERATION argument was given";
	return(-1);
  }

  log_msg((LOG_DEBUG_LEVEL, "OPERATION=\"%s\"", operation));
  if (strcaseeq(operation, "current"))
	op = TOKEN_OP_CURRENT;
  else if (strcaseeq(operation, "set_pin"))
	op = TOKEN_OP_SET_PIN;
  else if (strcaseeq(operation, "sync"))
	op = TOKEN_OP_SYNC;
  else if (strcaseeq(operation, "demo_create")) {
	op = TOKEN_OP_DEMO_CREATE;
	is_demo = 1;
  }
  else if (strcaseeq(operation, "demo_sync")) {
	op = TOKEN_OP_DEMO_SYNC;
	is_demo = 1;
  }
  else if (strcaseeq(operation, "demo_validate")) {
	op = TOKEN_OP_DEMO_VALIDATE;
	is_demo = 1;
  }
  else {
	*errmsg = ds_xprintf("Unrecognized OPERATION: \"%s\"", operation);
	return(-1);
  }

  username = kwv_lookup_value(kwv, "USERNAME");
  mode_str = kwv_lookup_value(kwv, "MODE");
  password = kwv_lookup_value(kwv, "PASSWORD");
  sync_str = kwv_lookup_value(kwv, "SYNC");
  pin = kwv_lookup_value(kwv, "PIN");
  new_pin = kwv_lookup_value(kwv, "NEW_PIN");
  confirm_new_pin = kwv_lookup_value(kwv, "CONFIRM_NEW_PIN");
  if (pin != NULL && *pin == '\0')
	pin = NULL;
  if (new_pin != NULL && *new_pin == '\0')
	new_pin = NULL;
  if (confirm_new_pin != NULL && *confirm_new_pin == '\0')
	confirm_new_pin = NULL;

  if (mode_str != NULL) {
	if (strcaseeq(mode_str, "hotp") || strcaseeq(mode_str, "counter"))
	  mode = TOKEN_MODE_COUNTER;
	else if (strcaseeq(mode_str, "totp") || strcaseeq(mode_str, "time"))
	  mode = TOKEN_MODE_TIME;
	else {
	  *errmsg = "Unrecognized MODE";
	  return(-1);
	}
  }
  else
	mode = TOKEN_MODE_UNKNOWN;

  if (is_demo) {
	log_msg((LOG_TRACE_LEVEL, "This is a demo operation"));
	auth_token_item_type = NULL;
	if (mode == TOKEN_MODE_COUNTER) {
	  if (vfs_lookup_item_type(ITEM_TYPE_AUTH_HOTP_TOKEN_DEMO) != NULL)
		auth_token_item_type = ITEM_TYPE_AUTH_HOTP_TOKEN_DEMO;
	}
	else if (mode == TOKEN_MODE_TIME) {
	  if (vfs_lookup_item_type(ITEM_TYPE_AUTH_TOTP_TOKEN_DEMO) != NULL)
		auth_token_item_type = ITEM_TYPE_AUTH_TOTP_TOKEN_DEMO;
	}

	if (auth_token_item_type == NULL) {
	  if (vfs_lookup_item_type(ITEM_TYPE_AUTH_TOKEN_DEMO) == NULL) {
		*errmsg = "The demo has been disabled";
		return(-1);
	  }
	  auth_token_item_type = ITEM_TYPE_AUTH_TOKEN_DEMO;
	}
  }
  else {
	if (mode == TOKEN_MODE_COUNTER) {
	  if (vfs_lookup_item_type(ITEM_TYPE_AUTH_HOTP_TOKEN) != NULL)
		auth_token_item_type = ITEM_TYPE_AUTH_HOTP_TOKEN;
	}
	else if (mode == TOKEN_MODE_TIME) {
	  if (vfs_lookup_item_type(ITEM_TYPE_AUTH_TOTP_TOKEN) != NULL)
		auth_token_item_type = ITEM_TYPE_AUTH_TOTP_TOKEN;
	}
  }
  log_msg((LOG_DEBUG_LEVEL, "auth_token_item_type=\"%s\"",
		   auth_token_item_type));

  if (op == TOKEN_OP_DEMO_CREATE) {
	unsigned int base, ndigits;
	char *key_str, *p, *serial_str;
	Auth_token_param param;
	Auth_hotp_param hotp_param;
	Auth_totp_param totp_param;

	if (mode == TOKEN_MODE_UNKNOWN) {
	  *errmsg = "The MODE argument is required";

	demo_failed:
	  html_header("Error");
	  printf("<b>An error occurred</b>: <em>%s</em>\n", *errmsg);
	  printf("<p><b>To continue,</b> ");
	  printf("<a href=\"javascript:history.back()\">return to the demo page</a>.\n");
	  emit_html_trailer(stdout);

	  exit(1);
	}

	if ((p = getenv("REMOTE_ADDR")) != NULL)
	  param.username = ds_xprintf("demo-%s%s",
								  p, (mode == TOKEN_MODE_COUNTER) ? "c" : "t");
	else
	  param.username = "demo17";
	param.mode = mode;
	param.item_type = auth_token_item_type;
	if (new_pin == NULL && confirm_new_pin == NULL)
	  param.pin = NULL;
	else if (new_pin != NULL && confirm_new_pin != NULL) {
	  if (!streq(new_pin, confirm_new_pin)) {
		*errmsg = "NEW_PIN and CONFIRM_NEW_PIN do not match";
		goto demo_failed;
	  }
	  param.pin = new_pin;
	}
	else {
	  *errmsg = "Require NEW_PIN and CONFIRM_NEW_PIN arguments";
	  goto demo_failed;
	}

	serial_str = kwv_lookup_value(kwv, "SERIAL");
	if ((param.serial = serial_str) == NULL)
	  param.serial = crypto_make_random_a64("Serial-", 8);

	key_str = kwv_lookup_value(kwv, "KEY");
	if ((param.key_str = key_str) == NULL) {
	  *errmsg = "The KEY argument is required";
	  goto demo_failed;
	}

	if ((p = kwv_lookup_value(kwv, "NDIGITS")) != NULL) {
	  if (strnum(p, STRNUM_UI, &ndigits) == -1
		  || ndigits < TOKEN_MIN_NDIGITS || ndigits > TOKEN_MAX_NDIGITS) {
		*errmsg = "Invalid NDIGITS";
		goto demo_failed;
	  }
	}
	else {
	  ndigits = 0;
	  log_msg((LOG_DEBUG_LEVEL, "Using default ndigits"));
	}

	if ((p = kwv_lookup_value(kwv, "BASE")) != NULL) {
	  if (strnum(p, STRNUM_UI, &base) == -1
		  || (base != 10 && base != 16 && base != 32)) {
		*errmsg = "Invalid BASE";
		goto demo_failed;
	  }
	}
	else {
	  base = 0;
	  log_msg((LOG_DEBUG_LEVEL, "Using default base"));
	}

	if (token_mode(mode, TOKEN_MODE_COUNTER)) {
	  char *counter_str;

	  if ((counter_str = kwv_lookup_value(kwv, "COUNTER")) == NULL) {
		counter_str = "0";
		log_msg((LOG_DEBUG_LEVEL, "Initializing counter to zero"));
	  }
	  hotp_param.counter_str = counter_str;
	  param.digest_name = "SHA1";			/* Not configurable. */
	  param.device.hotp = &hotp_param;
	  if (ndigits)
		param.ndigits = ndigits;
	  else
		param.ndigits = TOKEN_HOTP_NDIGITS;
	  if (base)
		param.base = base;
	  else
		param.base = TOKEN_HOTP_BASE;
	}
	else {
	  unsigned int time_step;
	  char *digest_str, *time_str;

	  if ((digest_str = kwv_lookup_value(kwv, "DIGEST_NAME")) != NULL) {
		Hmac_digest *hd;

		if ((hd = hmac_lookup_digest_by_name(digest_str)) == NULL
			|| (hd->alg != HMAC_SHA1
				&& hd->alg != HMAC_SHA256
				&& hd->alg != HMAC_SHA512)) {
		  *errmsg = "Invalid hash algorithm";
		  goto demo_failed;
		}
		param.digest_name = digest_str;
	  }
	  else
		param.digest_name = TOKEN_TOTP_DEFAULT_HASH;

	  if ((time_str = kwv_lookup_value(kwv, "TIME_STEP")) != NULL) {
		if (strnum(time_str, STRNUM_UINZ, &time_step) == -1) {
		  *errmsg = "Invalid TOTP time step";
		  goto demo_failed;
		}
		totp_param.time_step = time_step;
	  }
	  else
		totp_param.time_step = TOKEN_TOTP_DEFAULT_TIME_STEP_SECS;

	  param.device.totp = &totp_param;
	  if (ndigits)
		param.ndigits = ndigits;
	  else
		param.ndigits = TOKEN_TOTP_NDIGITS;
	  if (base)
		param.base = base;
	  else
		param.base = TOKEN_TOTP_BASE;
	}

	html_header("Result");
	st = auth_token_demo_create(&param);
	if (st == -1) {
	  *errmsg = "Demo account could not be created";
	  goto demo_failed;
	}
	emit_html_trailer(stdout);

	return(0);
  }

  if (username == NULL || pw_check_username(username) == -1) {
    *errmsg = "Invalid USERNAME argument";
	if (is_demo)
	  goto demo_failed;
    return(-1);
  }

  if (op == TOKEN_OP_DEMO_SYNC) {
	if (sync_str == NULL || (dsv_sync = strsplit(sync_str, ",", 0)) == NULL) {
	  *errmsg = "Missing or invalid SYNC argument";
	  goto demo_failed;
	}

	st = auth_token_sync(auth_token_item_type, username, dsv_sync, pin, errmsg);
	if (st == -1) {
	  *errmsg = "Synchronization failed";
	  goto demo_failed;
	}

	html_header("Result");
	printf("<p><b>Your account has been synchronized</b>.\n");
	printf("<p><b>To continue,</b> ");
	printf("<a href=\"javascript:history.back()\">return to the demo page</a>.\n");
	emit_html_trailer(stdout);

	return(0);
  }

  if (op == TOKEN_OP_DEMO_VALIDATE) {
	unsigned int hotp_accept_window;
	Auth_token *token;

	if (password == NULL) {
	  *errmsg = "A PASSWORD argument is required";
	  goto demo_failed;
	}

	if ((st = conf_val_uint(CONF_TOKEN_HOTP_ACCEPT_WINDOW,
							&hotp_accept_window)) == 0)
	  hotp_accept_window = TOKEN_HOTP_DEFAULT_ACCEPT_WINDOW;

	token = auth_token_get(auth_token_item_type, username);
	if (token == NULL) {
	  *errmsg = ds_xprintf("No account found for username \"%s\"", username);
	  goto demo_failed;
	}

	if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	  st = auth_hotp_token_validate(token, password, pin,
									TOKEN_REQUIRE_PIN, hotp_accept_window);
	  if (st == 0) {
		if ((st = auth_token_set(token)) == -1)
		  *errmsg = "Update failed";
	  }
	}
	else {
	  int drift;

	  st = auth_totp_token_validate(token, password, pin,
									TOKEN_IGNORABLE_PIN, 
									token->time_step, token->drift,
									TOKEN_TOTP_DEFAULT_DRIFT_WINDOW,
									token->ndigits, token->base, &drift);
	}
	if (st == -1) {
	  *errmsg = "Validation failed";
	  goto demo_failed;
	}

	html_header("Result");
	printf("<p><b>Validation was successful</b>.\n");
	printf("<p><b>To continue,</b> ");
	printf("<a href=\"javascript:history.back()\">return to the demo page</a>.\n");
	emit_html_trailer(stdout);

	return(0);
  }

  /*
   * Carefully check if the request should be permitted.
   * For SET_PIN, all is well if:
   *   o the user has credentials for USERNAME
   *   o the user has admin credentials
   * For SYNC, all is well if:
   *   o the user has admin credentials
   *   o the user provides the PIN (if the account has one) 
   */
  if (get_cookies(NULL, &cookies, &ncookies) == -1) {
	*errmsg = "Cookie parse error";
	return(-1);
  }

  if ((st = get_valid_scredentials(cookies, remote_addr, 0, &credentials,
								   &selected, NULL)) == -1) {
	*errmsg = "Invalid credentials";
	return(-1);
  }

  token = NULL;
  if (st == 0) {
	unsigned int hotp_accept_window;

	if (password == NULL) {
	  *errmsg = "No credentials or OTP sequence provided";
	  return(-1);
	}

	if ((st = conf_val_uint(CONF_TOKEN_HOTP_ACCEPT_WINDOW,
							&hotp_accept_window)) == 0)
	  hotp_accept_window = TOKEN_HOTP_DEFAULT_ACCEPT_WINDOW;
	else if (st == -1) {
	  *errmsg = "Invalid HOTP_ACCEPT_WINDOW";
	  return(-1);
	}

	if ((token = auth_token_get(auth_token_item_type, username)) == NULL) {
	  *errmsg = ds_xprintf("Cannot find username: \"%s\"", username);
	  return(-1);
	}

	st = auth_token_validate(token, password, pin, TOKEN_IGNORABLE_PIN,
							 NULL, hotp_accept_window);
	if (st != 0) {
	  *errmsg = "Invalid OTP sequence";
	  return(-1);
	}

	admin = is_dacs_admin_identity(NULL, NULL, token->username);
  }
  else {
	admin = is_dacs_admin(selected);

	user_ident = ds_xprintf("%s:%s", conf_val(CONF_JURISDICTION_NAME),
							username);
	st = is_matching_user_identity(user_ident, selected, DACS_NAME_CMP_CONFIG,
								   errmsg);
	if (st == -1 || (!admin && st == 0)) {
	  *errmsg = "Access denied";
	  return(-1);
	}
  }

  /* The request has been authorized. */
  switch (op) {
  case TOKEN_OP_CURRENT:
	if (!admin) {
	  *errmsg = "This operation is restricted to an administrator";
	  return(-1);
	}

	{
	  char *hval, *mf;

	  st = auth_token_get_current(auth_token_item_type, username, &hval, &mf,
								  errmsg);
	  if (st == -1)
		return(-1);
	  emit_plain_header(stdout);
	  printf("%s %s\n", mf, hval);
	  emit_plain_trailer(stdout);

	  return(0);
	}

	/*NOTREACHED*/

  case TOKEN_OP_SET_PIN:
	if (admin) {
	  if (new_pin == NULL && confirm_new_pin == NULL) {
		if (conf_val(CONF_TOKEN_REQUIRES_PIN)) {
		  *errmsg = "A PIN is required";
		  return(-1);
		}
	  }
	  else if (new_pin == NULL || confirm_new_pin == NULL
			   || !streq(new_pin, confirm_new_pin)) {
		*errmsg = "Invalid PIN";
		return(-1);
	  }
	}
	else {
	  if (new_pin == NULL || confirm_new_pin == NULL
		  || !streq(new_pin, confirm_new_pin)) {
		*errmsg = "Invalid PIN";
		return(-1);
	  }
	}

	/* For HOTP, this will also update the counter. */
	if (token == NULL)
	  st = auth_token_set_pin(auth_token_item_type, username, new_pin, errmsg);
	else
	  st = set_pin(token, new_pin, errmsg);

	break;

  case TOKEN_OP_SYNC:
	if (sync_str == NULL || (dsv_sync = strsplit(sync_str, ",", 0)) == NULL) {
	  *errmsg = "Missing or invalid SYNC argument";
	  return(-1);
	}

	if (token == NULL)
	  st = auth_token_sync(auth_token_item_type, username, dsv_sync, pin,
						   errmsg);
	else
	  st = token_sync(token, dsv_sync, errmsg);

	break;

  default:
	*errmsg = "Internal error";
	return(-1);
  }

  if (st == -1)
	return(-1);

  html_header("Result");
  printf("\nOperation succeeded\n");
  emit_html_trailer(stdout);

  return(0);
}

static void
dacs_usage(void)
{
  Dsvec *dsv_sorted;
  Strtable *stab;

  stab = strtable_init(2, 1, 1);

  fprintf(stderr, "Usage: dacstoken [dacsoptions] [flag ...] [username]\n");
  fprintf(stderr, "dacsoptions: %s\n", standard_command_line_usage);

  strtable_add_row(stab, "-all:", "with -set, apply change to all accounts");
  strtable_add_row(stab, "-auth otp:", "authenticate against password");
  strtable_add_row(stab, "-base #:", "radix for OTP value");
  strtable_add_row(stab, "-convert file:", "import file, convert, export");
  strtable_add_row(stab, "-counter #:", "counter value to set, if not zero");
  strtable_add_row(stab, "-create:", "create account for username");
  strtable_add_row(stab, "-current:", "display current OTP for username");
  strtable_add_row(stab, "-delete:", "delete account of username");
  strtable_add_row(stab, "-delpin:", "delete PIN for account of username");
  strtable_add_row(stab, "-digits #:", "number of digits for OTP value");
  strtable_add_row(stab, "-disable:", "disable account for username");
  strtable_add_row(stab, "-enable:", "enable account for username");
  strtable_add_row(stab, "-export:", "export account token accounts");
  strtable_add_row(stab, "-h | -help:", "display usage message");
  strtable_add_row(stab, "-hotp-show #:", "display # consecutive passwords");
  strtable_add_row(stab, "-hotp-window #:", "symmetric accept window size");
  strtable_add_row(stab, "-import file:", "add new accounts");
  strtable_add_row(stab, "-import-replace file:", "add/replacement accounts");
  strtable_add_row(stab, "-inkeys item_type:",
				   ds_xprintf("decryption keys (default: %s)",
							  inkeys_item_type));
  strtable_add_row(stab, "-key keyval:", "use keyval as the secret key");
  strtable_add_row(stab, "-key-file filename:", "read the secret key");
  strtable_add_row(stab, "-key-prompt:", "prompt for the secret key");
  strtable_add_row(stab, "-l | -list:", "list one or all users");
  strtable_add_row(stab, "-long:", "more detailed list");
  strtable_add_row(stab, "-mode otp-mode:", "OTP mode to use");
  strtable_add_row(stab, "-outkeys item_type:",
				   ds_xprintf("encryption keys (default: %s)",
							  outkeys_item_type));
  strtable_add_row(stab, "-pin val:", "use pinval as the secret PIN");
  strtable_add_row(stab, "-pin-constraints str:", "PIN format requirements");
  strtable_add_row(stab, "-pin-file filename:", "read the secret PIN");
  strtable_add_row(stab, "-pin-prompt:", "prompt for the secret PIN");
  strtable_add_row(stab, "-rename new_username:", "rename the account");
  strtable_add_row(stab, "-rng:", "use pseudo random numbers");
  strtable_add_row(stab, "-seed str:", "use str as initial seed");
  strtable_add_row(stab, "-serial str:", "serial number to get, list, or set");
  strtable_add_row(stab, "-set:", "modify account for username");
  strtable_add_row(stab, "-sync pwd-list:",
				   "attempt to synchronize HOTP count");
  strtable_add_row(stab, "-test:", "perform a self-test");
  strtable_add_row(stab, "-totp-base #:", "radix to use with TOTP");
  strtable_add_row(stab, "-totp-delta #:", "adjust TOTP time by # intervals");
  strtable_add_row(stab, "-totp-drift #:",
				   "TOTP symmetric clock drift window size");
  strtable_add_row(stab, "-totp-hash alg:", "Use alg as the TOTP digest");
  strtable_add_row(stab, "-totp-show #:", "display TOTP values");
  strtable_add_row(stab, "-totp-timestep secs:",
				   "TOTP interval width, in seconds");
  strtable_add_row(stab, "-validate otp:", "check a one-time password");
  strtable_add_row(stab, "-vfs vfs_uri:",
				   ds_xprintf("use vfs_uri instead of item type %s",
							  auth_token_item_type));

  dsv_sorted = strtable_sort(stab, 0, NULL);
  strtable_format(stderr, stab, dsv_sorted);

  exit(1);
}

int
dacstoken_main(int argc, char **argv, int do_init, void *main_out)
{
  int i, n, st, use_rng;
  int do_current, do_test, do_delete, do_long, do_set, do_totp_show;
  int do_hotp_show, do_list, do_import, do_import_replace, do_sync, do_validate;
  int do_auth, do_convert, do_create, do_delpin, do_export, do_rename;
  int get_key, get_pin;
  int saw_clock_delta, saw_hotp_flag, saw_totp_flag;
  int saw_base, saw_counter, saw_digits, saw_pin, saw_time_step;
  int token_requires_pin, saw_all, saw_disable, saw_enable;
  int totp_clock_delta;
  unsigned int base, hotp_base, hotp_ndigits, ndigits, totp_base, totp_ndigits;
  unsigned int hotp_show_count, show_length, totp_show_count, totp_time_step;
  unsigned int totp_drift_window, hotp_accept_window, slen;
  char *counter_str, *key_file, *key_str, *serial_str, *seed, *totp_digest_name;
  char *errmsg, *given_otp_value, *import_file, *username, *vfs_uri_alt;
  char *pin_constraints, *pin_file, *pin_str, *new_username, *vfs_uri_dst;
  char *remote_addr;
  unsigned char *secret;
  Auth_token *token;
  Auth_token_mode mode;
  Auth_token_param token_param;
  Auth_hotp_param hotp_param;
  Auth_totp_param totp_param;
  DACS_app_type app_type;
  Dsvec *dsv_sync;
  Kwv *kwv;
  Proc_lock *lock = NULL;

  pin_length = AUTH_GRID_PIN_LENGTH;
  mode = TOKEN_MODE_UNKNOWN;
  do_auth = do_convert = do_current = do_test = do_totp_show = do_hotp_show = 0;
  do_export = do_set = do_long = do_delete = do_list = do_validate = 0;
  do_import = do_import_replace = do_sync = 0;
  do_create = do_delpin = do_rename = 0;
  get_key = get_pin = use_rng = 0;
  base = ndigits = 0;
  saw_base = saw_counter = saw_digits = saw_pin = saw_time_step = 0;
  saw_clock_delta = saw_hotp_flag = saw_totp_flag = 0;
  saw_all = saw_disable = saw_enable = 0;
  hotp_accept_window = TOKEN_HOTP_DEFAULT_ACCEPT_WINDOW;
  hotp_ndigits = TOKEN_HOTP_NDIGITS;
  hotp_base = TOKEN_HOTP_BASE;
  totp_ndigits = TOKEN_TOTP_NDIGITS;
  totp_base = TOKEN_TOTP_BASE;
  totp_drift_window = TOKEN_TOTP_DEFAULT_DRIFT_WINDOW;
  totp_digest_name = TOKEN_TOTP_DEFAULT_HASH;
  totp_time_step = TOKEN_TOTP_DEFAULT_TIME_STEP_SECS;
  totp_clock_delta = 0;
  import_file = NULL;
  pin_constraints = NULL;
  secret = NULL;
  slen = 0;
  seed = NULL;
  serial_str = NULL;
  given_otp_value = NULL;
  counter_str = key_str = key_file = pin_file = pin_str = NULL;
  username = new_username = NULL;
  dsv_sync = NULL;
  show_length = 3;
  vfs_uri_alt = vfs_uri_dst = NULL;

  errmsg = "Internal error";

  if ((remote_addr = getenv("REMOTE_ADDR")) == NULL) {
    app_type = DACS_UTILITY_OPT;
    log_module_name = "dacstoken";
  }
  else {
    app_type = DACS_WEB_SERVICE;
    log_module_name = "dacs_token";
  }

  kwv = NULL;
  if (dacs_init(app_type, &argc, &argv, &kwv, &errmsg) == -1) {
  fail:
	log_msg((LOG_ERROR_LEVEL, "%s", errmsg));
	if (test_emit_format(EMIT_FORMAT_HTML)) {
      emit_html_header_status_line(stdout, "400", errmsg);
      printf("<b>Request failed</b>: <em>%s</em>\n", errmsg);
      emit_html_trailer(stdout);
    }
    else {
	  fprintf(stderr, "%s\n", errmsg);
      dacs_usage();
      /*NOTREACHED*/
    }

    return(-1);
  }

  if (sizeof(Auth_token_counter) < TOKEN_HOTP_COUNTER_LENGTH) {
	errmsg = ds_xprintf("Build error! sizeof(Auth_token_counter) < %d",
						TOKEN_HOTP_COUNTER_LENGTH);
	goto fail;
  }

  /* XXX This course-grained lock prevents concurrent access. */
  if ((lock = proc_lock_create(PROC_LOCK_TOKEN)) == NULL) {
    log_msg((LOG_ERROR_LEVEL, "Can't set lock"));
    errmsg = "Can't set lock";
    goto fail;
  }

  /* Here is where the command version splits from the web service version. */
  if (app_type == DACS_WEB_SERVICE) {
    if ((st = dacs_token(kwv, remote_addr, &errmsg)) == -1)
	  goto fail;

    return(0);
  }

  if (!dacs_saw_command_line_log_level)
	log_set_level(NULL, LOG_WARN_LEVEL);

  for (i = 1; i < argc; i++) {
	if (argv[i][0] != '-')
	  break;

	if (streq(argv[i], "-all"))
	  saw_all = 1;
	else if (streq(argv[i], "-auth")) {
	  if (++i == argc) {
		log_msg((LOG_ERROR_LEVEL, "OTP value expected"));
		goto fail;
	  }
	  given_otp_value = argv[i];
	  do_auth = 1;
	}
	else if (streq(argv[i], "-base")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &base) == -1
		  || (base != 10 && base != 16 && base != 32)) {
		log_msg((LOG_ERROR_LEVEL, "Invalid base"));
		goto fail;
	  }
	  saw_base++;
	}
	else if (streq(argv[i], "-convert")) {
	  if (++i == argc)
		goto fail;
	  do_convert = 1;
	  import_file = argv[i];
	}
	else if (streq(argv[i], "-counter")) {
	  if (++i == argc)
		goto fail;
	  counter_str = argv[i];
	  saw_counter++;
	  saw_hotp_flag++;
	}
	else if (streq(argv[i], "-create"))
	  do_create = 1;
	else if (streq(argv[i], "-current"))
	  do_current = 1;
	else if (streq(argv[i], "-debug"))
	  auth_token_debug = 1;
	else if (streq(argv[i], "-delete"))
	  do_delete = 1;
	else if (streq(argv[i], "-delpin"))
	  do_delpin = 1;
	else if (streq(argv[i], "-digits")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &ndigits) == -1
		  || ndigits < TOKEN_MIN_NDIGITS || ndigits > TOKEN_MAX_NDIGITS) {
		log_msg((LOG_ERROR_LEVEL, "Invalid digits"));
		goto fail;
	  }
	  saw_digits++;
	}
	else if (streq(argv[i], "-disable"))
	  saw_disable = 1;
	else if (streq(argv[i], "-enable"))
	  saw_enable = 1;
	else if (streq(argv[i], "-export"))
	  do_export = 1;
	else if (streq(argv[i], "-h") || streq(argv[i], "-help"))
	  goto fail;
	else if (streq(argv[i], "-hotp-show")) {
	  do_hotp_show = 1;
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &hotp_show_count) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid -hotp-show count"));
		goto fail;
	  }
	}
	else if (streq(argv[i], "-hotp-window")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &hotp_accept_window) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid HOTP accept window"));
		goto fail;
	  }
	  saw_hotp_flag++;
	}
	else if (streq(argv[i], "-import")) {
	  if (++i == argc || do_import_replace)
		goto fail;
	  do_import = 1;
	  import_file = argv[i];
	}
	else if (streq(argv[i], "-import-replace")) {
	  if (++i == argc || do_import)
		goto fail;
	  do_import_replace = 1;
	  import_file = argv[i];
	}
	else if (streq(argv[i], "-inkeys")) {
	  if (++i == argc)
		goto fail;
	  inkeys_item_type = argv[i];
	}
	else if (streq(argv[i], "-key")) {
	  if (++i == argc || key_str != NULL || get_key)
		goto fail;
	  key_str = strdup(argv[i]);
	  strzap(argv[i]);
	}
	else if (streq(argv[i], "-key-file")) {
	  if (++i == argc || get_key || key_str != NULL)
		goto fail;
	  key_file = argv[i];
	  get_key = 1;
	}
	else if (streq(argv[i], "-key-prompt")) {
	  if (get_key || key_str != NULL)
		goto fail;
	  key_file = NULL;
	  get_key = 1;
	}
	else if (streq(argv[i], "-list") || streq(argv[i], "-l")) {
	  if (do_list)
		do_long++;
	  else
		do_list = 1;
	}
	else if (streq(argv[i], "-long")) {
	  do_long++;
	  do_list = 1;
	}
	else if (streq(argv[i], "-mode")) {
	  if (++i == argc || mode != TOKEN_MODE_UNKNOWN)
		goto fail;
	  if (strcaseeq(argv[i], "counter") || strcaseeq(argv[i], "hotp"))
		mode |= TOKEN_MODE_COUNTER;
	  else if (strcaseeq(argv[i], "time") || strcaseeq(argv[i], "totp"))
		mode |= TOKEN_MODE_TIME;
	  else {
		errmsg = "Unrecognized mode";
		goto fail;
	  }
	}
	else if (streq(argv[i], "-outkeys")) {
	  if (++i == argc)
		goto fail;
	  outkeys_item_type = argv[i];
	}
	else if (streq(argv[i], "-pin")) {
	  if (++i == argc || pin_str != NULL || get_pin)
		goto fail;
	  pin_str = strdup(argv[i]);
	  strzap(argv[i]);
	  saw_pin = 1;
	}
	else if (streq(argv[i], "-pin-constraints")) {
	  if (++i == argc || pin_constraints != NULL)
		goto fail;
	  pin_constraints = strdup(argv[i]);
	}
	else if (streq(argv[i], "-pin-file")) {
	  if (++i == argc || get_pin || pin_str != NULL)
		goto fail;
	  pin_file = argv[i];
	  get_pin = 1;
	  saw_pin = 1;
	}
	else if (streq(argv[i], "-pin-prompt")) {
	  if (get_pin || pin_str != NULL)
		goto fail;
	  pin_file = NULL;
	  get_pin = 1;
	  saw_pin = 1;
	}
	else if (streq(argv[i], "-rename")) {
	  if (++i == argc || new_username != NULL)
		goto fail;
	  new_username = argv[i];
	  do_rename = 1;
	}
	else if (streq(argv[i], "-rng"))
	  use_rng = 1;
	else if (streq(argv[i], "-seed")) {
	  if (++i == argc)
		goto fail;
	  seed = argv[i];
	}
	else if (streq(argv[i], "-serial")) {
	  if (++i == argc)
		goto fail;
	  serial_str = argv[i];
	}
	else if (streq(argv[i], "-set"))
	  do_set = 1;
	else if (streq(argv[i], "-sync")) {
	  if (do_sync || ++i == argc)
		goto fail;
	  dsv_sync = strsplit(argv[i], ",", 0);
	  do_sync = 1;
	}
	else if (streq(argv[i], "-test"))
	  do_test = 1;
	else if (streq(argv[i], "-totp-show")) {
	  do_totp_show = 1;
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &totp_show_count) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid -totp-show count"));
		goto fail;
	  }
	}
	else if (streq(argv[i], "-totp-delta")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_I, &totp_clock_delta) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid TOTP clock delta"));
		goto fail;
	  }
	  saw_clock_delta++;
	}
	else if (streq(argv[i], "-totp-hash")) {
	  Hmac_digest *hd;

	  if (++i == argc)
		goto fail;
	  totp_digest_name = argv[i];
	  if ((hd = hmac_lookup_digest_by_name(totp_digest_name)) == NULL
		  || (hd->alg != HMAC_SHA1
			  && hd->alg != HMAC_SHA256
			  && hd->alg != HMAC_SHA512)) {
		log_msg((LOG_ERROR_LEVEL, "Invalid hash algorithm"));
		goto fail;
	  }
	  saw_totp_flag++;
	}
	else if (streq(argv[i], "-totp-drift")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UI, &totp_drift_window) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid TOTP drift window"));
		goto fail;
	  }
	  saw_totp_flag++;
	}
	else if (streq(argv[i], "-totp-timestep")) {
	  if (++i == argc)
		goto fail;
	  if (strnum(argv[i], STRNUM_UINZ, &totp_time_step) == -1) {
		log_msg((LOG_ERROR_LEVEL, "Invalid TOTP time step"));
		goto fail;
	  }
	  saw_time_step++;
	  saw_totp_flag++;
	}
	else if (streq(argv[i], "-validate")) {
	  if (++i == argc) {
		log_msg((LOG_ERROR_LEVEL, "OTP value expected"));
		goto fail;
	  }
	  given_otp_value = argv[i];
	  do_validate = 1;
	}
	else if (streq(argv[i], "-vfs")) {
	  if (++i == argc || vfs_uri_alt != NULL)
		goto fail;
	  vfs_uri_alt = argv[i];
	}
	else
	  goto fail;
  }

  if (argv[i] != NULL) {
	username = argv[i++];
	if (i != argc)
	  goto fail;
	if (!is_valid_username(username)) {
	  errmsg = ds_xprintf("Invalid username: \"%s\"", username);
	  goto fail;
	}
  }

  if (new_username != NULL && !is_valid_username(new_username)) {
	errmsg = ds_xprintf("Invalid new username: \"%s\"", new_username);
	goto fail;
  }

  if ((saw_enable + saw_disable) > 1)
	goto fail;

#ifdef NOTDEF
  if (use_rng) {
	if (seed == NULL)
	  goto fail;
	rng_state = rng_init(seed);
  }
#endif

  n = do_auth + do_create + do_current + do_delete + do_export + do_list
	+ do_import + do_import_replace + do_set + do_sync + do_validate + do_test
	+ do_hotp_show + do_totp_show + do_convert + do_delpin + do_rename;
  if (n > 1) {
	errmsg = "More than one operation has been specified";
	goto fail;
  }

  if (n == 0) {
	if (saw_enable || saw_disable) {
	  errmsg = "Invalid flag";
	  goto fail;
	}
	do_list = 1;
  }

  if (username != NULL && saw_all && !do_set)
	goto fail;

  if (conf_val(CONF_TOKEN_REQUIRES_PIN) == NULL
	  || conf_val_eq(CONF_TOKEN_REQUIRES_PIN, "yes"))
	token_requires_pin = 1;
  else if (conf_val_eq(CONF_TOKEN_REQUIRES_PIN, "no"))
	token_requires_pin = 0;
  else {
	errmsg = "Invalid TOKEN_REQUIRES_PIN directive";
	goto fail;
  }

  if (vfs_uri_alt != NULL) {
	if ((auth_token_item_type = add_vfs(vfs_uri_alt)) == NULL)
	  goto fail;
  }

  if (mode == TOKEN_MODE_UNKNOWN) {
	if (do_totp_show || saw_totp_flag)
	  mode = TOKEN_MODE_TIME;
	else if (do_hotp_show || saw_hotp_flag)
	  mode = TOKEN_MODE_COUNTER;
  }

  if ((token_mode(mode, TOKEN_MODE_COUNTER) && saw_totp_flag)
	  || (token_mode(mode, TOKEN_MODE_TIME) && saw_hotp_flag)
	  || (saw_hotp_flag && saw_totp_flag)) {
	errmsg = "Invalid flag combination - mixed modes";
	goto fail;
  }

  if (do_create) {
	if (serial_str == NULL) {
	  errmsg = "A serial number is required";
	  goto fail;
	}

	if (get_key == 0 && key_str == NULL) {
	  errmsg = "A key is required";
	  goto fail;
	}

	if (token_mode(mode, TOKEN_MODE_UNKNOWN)) {
	  errmsg = "A device mode is required";
	  goto fail;
	}

	if (pin_str == NULL && !get_pin) {
	  if (token_requires_pin) {
		errmsg = "This account must be created with a PIN";
		goto fail;
	  }
	}
  }

  if (do_current) {
	char *hval, *mf;

	if (username == NULL) {
	  errmsg = "A username is required";
	  goto fail;
	}
	token = auth_token_get(auth_token_item_type, username);
	if (token == NULL) {
	  errmsg = ds_xprintf("No account found for username \"%s\"", username);
	  goto fail;
	}

	if (auth_token_current(token, &hval, &mf) == -1) {
	  errmsg = "Could not get current OTP";
	  goto fail;
	}

	printf("%s %s\n", mf, hval);

	exit(0);
  }

  if (token_mode(mode, TOKEN_MODE_COUNTER)) {
	if (saw_digits)
	  hotp_ndigits = ndigits;
	if (saw_base)
	  hotp_base = base;
	if (vfs_lookup_item_type(ITEM_TYPE_AUTH_HOTP_TOKEN) != NULL)
	  auth_token_item_type = ITEM_TYPE_AUTH_HOTP_TOKEN;
  }
  else {
	if (saw_digits)
	  totp_ndigits = ndigits;
	if (saw_base)
	  totp_base = base;
	if (vfs_lookup_item_type(ITEM_TYPE_AUTH_TOTP_TOKEN) != NULL)
	  auth_token_item_type = ITEM_TYPE_AUTH_TOTP_TOKEN;
  }

  if (get_key) {
	if (key_file == NULL) {
	  key_str = pw_prompt_new_password(username, "token key", &errmsg);
	  if (key_str == NULL)
		goto fail;
	}
	else if (streq(key_file, "-"))
	  key_str = get_passwd(GET_PASSWD_STDIN, NULL);
	else
	  key_str = get_passwd(GET_PASSWD_FILE, key_file);
  }

  if (key_str != NULL) {
	if (strlen(key_str) < TOKEN_MIN_KEY_LENGTH_BYTES) {
	  errmsg = ds_xprintf("Key is too short, minimum is %d bytes",
						  TOKEN_MIN_KEY_LENGTH_BYTES);
	  goto fail;
	}
	if ((secret = strhextob(key_str, &slen)) == NULL) {
	  errmsg = "Invalid key";
	  goto fail;
	}
  }

  if (pin_constraints == NULL)
	pin_constraints = conf_val(CONF_PASSWORD_CONSTRAINTS);

  if (get_pin) {
	/* A PIN is needed, and it wasn't given on the command line. */
	if (pin_file == NULL) {
	  errmsg = NULL;
	  if (do_set) {
		/* It needs to be retyped when being set. */
		pin_str = pw_prompt_new_password(username, "account PIN", &errmsg);
	  }
	  else
		pin_str = get_passwd(GET_PASSWD_PROMPT, "Account PIN? ");
	  if (pin_str == NULL) {
		if (errmsg == NULL)
		  errmsg = "Did not read PIN";
		goto fail;
	  }
	}
	else if (streq(pin_file, "-"))
	  pin_str = get_passwd(GET_PASSWD_PROMPT, "account PIN? ");
	else
	  pin_str = get_passwd(GET_PASSWD_FILE, pin_file);

	if (!pw_is_passwd_acceptable(pin_str, pin_constraints)) {
	  errmsg = "PIN does not meet constraints";
	  goto fail;
	}
  }

  if (do_totp_show) {
	if (username != NULL) {
	  token = auth_token_get(auth_token_item_type, username);
	  if (token == NULL) {
		errmsg = ds_xprintf("No account found for username \"%s\"", username);
		goto fail;
	  }
	  if (!token_mode(token->mode, TOKEN_MODE_TIME)) {
		errmsg = ds_xprintf("Account for username \"%s\" is not HOTP",
							username);
		goto fail;
	  }
	  if (secret == NULL) {
		secret = token->key;
		slen = token->keylen;
	  }

	  if (!saw_digits && token->ndigits)
		totp_ndigits = token->ndigits;
	  if (!saw_base && token->base)
		totp_base = token->base;
	  if (!saw_time_step && token->time_step)
		totp_time_step = token->time_step;
	}
	else
	  token = NULL;

	totp_now(token, secret, slen, totp_show_count, totp_digest_name,
			 totp_time_step, totp_ndigits, totp_base);

	exit(0);
  }

  if (do_hotp_show) {
	unsigned int ui;
	char *hval;
	unsigned char *counter;
	Ds *ds;

	if (username != NULL) {
	  token = auth_token_get(auth_token_item_type, username);
	  if (token == NULL) {
		errmsg = ds_xprintf("No account found for username \"%s\"", username);
		goto fail;
	  }
	  if (!token_mode(token->mode, TOKEN_MODE_COUNTER)) {
		errmsg = ds_xprintf("Account for username \"%s\" is not HOTP",
							username);
		goto fail;
	  }
	  if (secret == NULL) {
		secret = token->key;
		slen = token->keylen;
	  }

	  if (counter_str == NULL)
		counter_str = token_format_counter(token->counter);
	  if (!saw_digits && token->ndigits)
		hotp_ndigits = token->ndigits;
	  if (!saw_base && token->base)
		hotp_base = token->base;
	}
	else {
	  if (secret == NULL) {
		errmsg = "A key is required";
		goto fail;
	  }
	}

	counter = strhextob(canonicalize_counter(counter_str), NULL);
	if (counter == NULL) {
	  errmsg = "Invalid counter: conversion error";
	  goto fail;
	}

	for (ui = 0; ui < hotp_show_count; ui++) {
	  hval = auth_hotp_value(secret, slen, counter, TOKEN_HOTP_COUNTER_LENGTH,
							 hotp_ndigits, hotp_base);
	  printf("%s: %s\n", token_format_counter(token->counter), hval);
	  token_inc_counter(counter);
	}

	exit(0);
  }

  if (do_test) {
	if (token_test() == -1)
	  exit(1);
	exit(0);
  }

  if (do_export) {
	if (auth_token_export(stdout, mode, username) == -1)
	  exit(1);
	exit(0);
  }

  if (do_import || do_import_replace || do_convert) {
	FILE *fp;

	if (streq(import_file, "-"))
	  fp = stdin;
	else {
	  if ((fp = fopen(import_file, "r")) == NULL) {
		errmsg = ds_xprintf("Cannot open \"%s\": %s",
							import_file, strerror(errno));
		goto fail;
	  }
	}

	if (do_convert) {
	  if (auth_token_convert(fp, mode) == -1)
		exit(1);
	}
	else if (auth_token_import(fp, mode, do_import_replace, pin_constraints)
			 == -1)
	  exit(1);

	exit(0);
  }

  if (do_list) {
	Dsvec *dsv;

	if (username != NULL) {
	  if ((token = auth_token_get(auth_token_item_type, username)) == NULL) {
		errmsg = ds_xprintf("No account found for username \"%s\"", username);
		goto fail;
	  }

	  if (mode == TOKEN_MODE_UNKNOWN
		  || token_mode(token->mode, mode & TOKEN_MODE_MASK)) {
		if (do_long)
		  auth_token_show(stderr, token);
		else
		  printf("%s\n", username);
	  }

	  return(0);
	}

	if (serial_str != NULL) {
	  if ((token = lookup_by_serial(serial_str)) == NULL) {
		errmsg = ds_xprintf("No account found for serial number \"%s\"",
							serial_str);
		goto fail;
	  }
	  if (mode == TOKEN_MODE_UNKNOWN
		  || token_mode(token->mode, mode & TOKEN_MODE_MASK)) {
		if (do_long)
		  auth_token_show(stderr, token);
		else
		  printf("%s\n", token->username);
	  }

	  return(0);
	}

	dsv = auth_token_list(auth_token_item_type);
	for (i = 0; i < dsvec_len(dsv); i++) {
	  char *u;

	  u = (char *) dsvec_ptr_index(dsv, i);
	  if (do_long) {
		if ((token = auth_token_get(auth_token_item_type, u)) == NULL) {
		  errmsg = ds_xprintf("Could not get account data for \"%s\"", u);
		  goto fail;
		}
		if (mode == TOKEN_MODE_UNKNOWN
			|| token_mode(token->mode, mode & TOKEN_MODE_MASK))
		  auth_token_show(stderr, token);
	  }
	  else
		printf("%s\n", u);
	}

	return(0);
  }

  if (do_sync) {
	unsigned char *counter;

	if (username != NULL) {
	  token = auth_token_get(auth_token_item_type, username);
	  if (token == NULL) {
		errmsg = ds_xprintf("No account found for username \"%s\"", username);
		goto fail;
	  }
	  mode = token->mode;
	  secret = token->key;
	  slen = token->keylen;
	}
	else if (token_mode(mode, TOKEN_MODE_UNKNOWN)) {
	  errmsg = "No mode has been specified";
	  goto fail;
	}

	if (token_mode(mode, TOKEN_MODE_TIME)) {
	  int diff, nintervals, offset;

	  if (secret == NULL) {
		errmsg = "A key is required";
		goto fail;
	  }
	  if (!saw_digits && token != NULL && token->ndigits)
		totp_ndigits = token->ndigits;
	  if (!saw_base && token != NULL && token->base)
		totp_base = token->base;

	  given_otp_value = (char *) dsvec_ptr_index(dsv_sync, 0);
	  st = totp_sync(given_otp_value, totp_digest_name, secret, slen,
						 totp_time_step, totp_ndigits, totp_base, &offset);
	  if (st == -1) {
		errmsg = "Could not synchronize";
		goto fail;
	  }
	  if (st == 0)
		fprintf(stderr, "Token is synchronized\n");
	  else {
		diff = (offset < 0) ? -offset : offset;
		nintervals = diff / totp_time_step;
		fprintf(stderr, "Token's clock is %s by about %d seconds\n",
				(offset < 0) ? "slow" : "fast", diff);
		fprintf(stderr, "Delta is %d %s at interval size %d secs\n",
				nintervals,
				(nintervals == 1) ? "interval" : "intervals",
				totp_time_step);
	  }

	  if (st > 0 && username != NULL) {
		token->drift = offset;
		if (auth_token_set(token) == -1) {
		  errmsg = "Could not update account";
		  goto fail;
		}
	  }

	  exit(0);
	}
	else if (!token_mode(mode, TOKEN_MODE_COUNTER)) {
	  errmsg = "Internal error - unknown mode";
	  goto fail;
	}

	if (counter_str != NULL) {
	  counter = strhextob(canonicalize_counter(counter_str), NULL);
	  if (counter == NULL) {
		errmsg = "Invalid counter: conversion error";
		goto fail;
	  }
	}
	else
	  counter = NULL;

	if (hotp_sync(token, dsv_sync, counter) == -1) {
	  errmsg = "Synchronization failed";
	  goto fail;
	}

	if (username != NULL) {
	  if (auth_token_set(token) == -1) {
		errmsg = "Could not update account";
		goto fail;
	  }
	}
	else {
	  /* Print the counter... */
	}

	return(0);
  }

  if (do_delete || do_delpin) {
	if (username == NULL)
	  goto fail;
	if (do_delpin) {
#ifdef NOTDEF
	  if (token_requires_pin) {
		errmsg = "This account must have a PIN";
		goto fail;
	  }
#endif
	  if ((token = auth_token_get(auth_token_item_type, username)) == NULL) {
		errmsg = ds_xprintf("No account found for username \"%s\"", username);
		goto fail;
	  }
	  token->pin_hash = NULL;
	  st = auth_token_set(token);
	}
	else
	  st = auth_token_delete(auth_token_item_type, username);

	if (st == -1) {
	  errmsg = ds_xprintf("Delete%s failed", do_delpin ? " PIN" : "");
	  goto fail;
	}

	return(0);
  }

  if (do_set || do_create || do_rename) {
	char *u;

	token = NULL;
	if (username == NULL) {
	  Dsvec *dsv;

	  if (do_create || do_rename) {
		errmsg = "A username is required";
		goto fail;
	  }
	  else if (!saw_all) {
		errmsg = "The either a username or the -all flag is required";
		goto fail;
	  }

	  /* Perform one or more changes to all accounts. */
	  dsv = auth_token_list(auth_token_item_type);
	  for (i = 0; i < dsvec_len(dsv); i++) {
		char *u;

		u = (char *) dsvec_ptr_index(dsv, i);
		if ((token = auth_token_get(auth_token_item_type, u)) == NULL) {
		  errmsg = ds_xprintf("Could not get account data for \"%s\"", u);
		  goto fail;
		}
		if (saw_disable)
		  token->mode |= TOKEN_MODE_DISABLED;
		else if (saw_enable)
		  token->mode &= (~TOKEN_MODE_DISABLED);

		if (auth_token_set(token) == -1) {
		  errmsg = ds_xprintf("Could not set account for \"%s\"", u);
		  goto fail;
		}
	  }

	  return(0);
	}

	if ((token = auth_token_get(auth_token_item_type, username)) == NULL) {
	  if (do_set || do_rename) {
		errmsg = ds_xprintf("Could not get account data for \"%s\"", username);
		goto fail;
	  }

	  /* Create a new account. */
	  if (serial_str == NULL) {
		errmsg = "A serial number is required";
		goto fail;
	  }
	  if (mode == TOKEN_MODE_UNKNOWN) {
		errmsg = "The -mode flag is required";
		goto fail;
	  }

	  /* Default is to create in the disabled state. */
	  if (saw_disable || !saw_enable)
		mode |= TOKEN_MODE_DISABLED;

	  if (serial_count(serial_str) != 0) {
		errmsg = ds_xprintf("Serial number error for \"%s\"", serial_str);
		goto fail;
	  }

	  u = username;
	}
	else {
	  int count;

	  if (do_create) {
		errmsg = ds_xprintf("Cannot create account for \"%s\", already exists",
							username);
		goto fail;
	  }

	  if (do_rename
		  && auth_token_get(auth_token_item_type, new_username) != NULL) {
		errmsg = ds_xprintf("Cannot create account for \"%s\", already exists",
							new_username);
		goto fail;
	  }

	  /*
	   * Update or rename an existing account.
	   * Note that the serial number might be changing.
	   * XXX it's possible to do a null-effect update.
	   */
	  if (serial_str != NULL
		  && (count = serial_count(serial_str)) != 0 && count != 1) {
		errmsg = ds_xprintf("Serial number error for \"%s\"", serial_str);
		goto fail;
	  }
	  u = token->username;
	  /* The operation mode can be changed, too. */
	  if (token_mode(mode, TOKEN_MODE_UNKNOWN))
		mode = token->mode;

	  if (saw_disable)
		mode |= TOKEN_MODE_DISABLED;
	  else if (saw_enable)
		mode &= (~TOKEN_MODE_DISABLED);

	  if (serial_str == NULL)
		serial_str = token->serial;

	  if (token_mode(token->mode, TOKEN_MODE_COUNTER)
		  && counter_str == NULL)
		counter_str = token_format_counter(token->counter);

	  if (key_str == NULL)
		key_str = strbtohex(token->key, token->keylen, 0);
	}

	if (key_str == NULL) {
	  errmsg = "A key is required";
	  goto fail;
	}

	if (do_rename)
	  token_param.username = new_username;
	else
	  token_param.username = u;

	token_param.mode = mode;
	token_param.item_type = auth_token_item_type;
	token_param.serial = serial_str;
	token_param.pin = pin_str;
	token_param.key_str = key_str;
	if (token_mode(mode, TOKEN_MODE_COUNTER)) {
	  hotp_param.counter_str = counter_str;
	  token_param.digest_name = "SHA1";			/* Not configurable. */
	  token_param.device.hotp = &hotp_param;
	  token_param.ndigits = hotp_ndigits;
	  token_param.base = hotp_base;
	}
	else {
	  token_param.digest_name = totp_digest_name;
	  totp_param.time_step = totp_time_step;
	  token_param.device.totp = &totp_param;
	  token_param.ndigits = totp_ndigits;
	  token_param.base = totp_base;
	}

	token = auth_token_init(&token_param);

	strzap(key_str);
	if (token == NULL)
	  goto fail;

	if (auth_token_set(token) == -1) {
	  errmsg = ds_xprintf("Could not set account for \"%s\"", username);
	  goto fail;
	}

	if (do_rename) {
	  if (auth_token_delete(auth_token_item_type, username) == -1) {
		errmsg = ds_xprintf("Could not delete account for \"%s\"", username);
		goto fail;
	  }
	}

	return(0);
  }

  st = 0;
  if (do_auth || do_validate) {
	int drift;
	unsigned char *counter;

	/*
	 * There are four possibilities:
	 *   HOTP with/without username
	 *   TOTP with/without username
	 * If a username is given, validation parameters
	 * (such as the key and counter value) are taken from the account but can
	 * be overridden on the command line.
	 * If we're authenticating, then check the PIN (if necessary) and
	 * advance the counter (HOTP).
	 */
	if (username == NULL) {
	  if (do_auth) {
		errmsg = "A username is required";
		goto fail;
	  }
	  if (secret == NULL) {
		errmsg = "A key is required";
		goto fail;
	  }

	  if (token_mode(mode, TOKEN_MODE_COUNTER)) {
		if (counter_str == NULL) {
		  errmsg = "The -counter flag is required";
		  goto fail;
		}
		counter = strhextob(canonicalize_counter(counter_str), NULL);
		if (counter == NULL) {
		  errmsg = "Invalid counter: conversion error";
		  goto fail;
		}

		st = hotp_validate(given_otp_value, secret, slen,
						   counter, TOKEN_HOTP_COUNTER_LENGTH,
						   hotp_ndigits, hotp_base, hotp_accept_window);
	  }
	  else {	
		st = totp_validate(given_otp_value, totp_digest_name, secret, slen,
						   totp_time_step, totp_clock_delta,
						   totp_drift_window, totp_ndigits, totp_base, &drift);
	  }

	  exit(st);
	}


	token = auth_token_get(auth_token_item_type, username);
	if (token == NULL) {
	  errmsg = ds_xprintf("No account found for username \"%s\"", username);
	  goto fail;
	}

	if (token_mode(token->mode, TOKEN_MODE_COUNTER)) {
	  st = auth_hotp_token_validate(token, given_otp_value, pin_str,
									TOKEN_REQUIRE_PIN, hotp_accept_window);
	  if (st == 0 && do_auth) {
		st = auth_token_update(token);
	  }
	}
	else {
	  if (!saw_digits && token->ndigits)
		totp_ndigits = token->ndigits;
	  if (!saw_base && token->base)
		totp_base = token->base;
	  if (!saw_time_step && token->time_step)
		totp_time_step = token->time_step;
	  if (!saw_clock_delta)
		totp_clock_delta = token->drift;

	  st = auth_totp_token_validate(token, given_otp_value, pin_str,
									TOKEN_REQUIRE_PIN, 
									totp_time_step, totp_clock_delta,
									totp_drift_window, totp_ndigits,
									totp_base, &drift);
	}
  }

  exit((st == 0) ? 0 : 1);
}

#else

int
main(int argc, char **argv)
{
  int rc;

  if ((rc = dacstoken_main(argc, argv, 1, NULL)) == 0)
	exit(0);

  exit(1);
}
#endif
