/* opielogin.c: "Login" code for OTP. Not to be confused with /bin/login.

Portions of this software are Copyright 1995 by Randall Atkinson and Dan
McDonald, All Rights Reserved. All Rights under this copyright are assigned
to the U.S. Naval Research Laboratory (NRL). The NRL Copyright Notice and
License Agreement applies to this software.

	History:

	Modified at NRL for OPIE 2.1. Made some changes for autoconf.
	Modified at NRL for OPIE 2.0.
	Written at Bellcore for the S/Key Version 1 software distribution
		(skeylogin.c).
*/
#include "opie_cfg.h"

#include <sys/param.h>
#include <sys/stat.h>
#if HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H
#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else /* TIME_WITH_SYS_TIME */
#if HAVE_SYS_TIME_H
#include <sys/time.h>
#else /* HAVE_SYS_TIME_H */
#include <time.h>
#endif /* HAVE_SYS_TIME_H */
#endif /* TIME_WITH_SYS_TIME */
#include <sys/resource.h>
#else /* HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H */
#if TM_IN_SYS_TIME
#include <sys/time.h>
#else /* TM_IN_SYS_TIME */
#include <time.h>
#endif /* TM_IN_SYS_TIME */
#endif /* HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H */

#ifdef	QUOTA
#include <sys/quota.h>
#endif

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>

#include "opie.h"

char *opielockfilename = NULL;

static int opieunlock __ARGS((void));

/** begin real code ***/

static char *opieskipspace(cp)
register char *cp;
{
  while (*cp == ' ' || *cp == '\t')
    cp++;

  if (*cp == '\0')
    return NULL;
  else
    return cp;
}

/* Convert 8-byte binary array to hex-ascii string */
int opiebtoa8(out, in)
char *out, *in;
{
  register int i;

  if (in == NULL || out == NULL)
    return -1;

  for (i = 0; i < 8; i++) {
    sprintf(out, "%02x", *in++ & 0xff);
    out += 2;
  }
  return 0;
}


/* Convert hex digit to binary integer */
static int opiehtoi(c)
register char c;
{
  if ('0' <= c && c <= '9')
    return c - '0';
  if ('a' <= c && c <= 'f')
    return 10 + c - 'a';
  if ('A' <= c && c <= 'F')
    return 10 + c - 'A';
  return -1;
}

/* atexit() handler for opielock() */
static void opieunlockaeh()
{
  if (opielockfilename)
    opieunlock();
}

/* 
   Serialize (we hope) authentication of user to prevent race conditions.
   Creates a lock file with a name of OPIE_LOCK_PREFIX with the user name
   appended. This file contains the pid of the lock's owner and a time()
   stamp. We use the former to check for dead owners and the latter to
   provide an upper bound on the lock duration. If there are any problems,
   we assume the lock is bogus.

   The value of this locking and its security implications are still not
   completely clear and require further study.

   One could conceivably hack this facility to provide locking of user
   accounts after several authentication failures.
 
   Return -1 on low-level error, 0 if ok, 1 on locking failure.
*/
static int opielock(name)
char *name;
{
#ifdef USER_LOCKING
  int fh, waits = 0, rval = -1, pid, t, i;
  char buffer[128], buffer2[128], *c, *c2;

  if (!(opielockfilename = malloc(sizeof(OPIE_LOCK_PREFIX) + strlen(name))))
    return -1;

  strcpy(opielockfilename, OPIE_LOCK_PREFIX);
  strcat(opielockfilename, name);

  fh = 0;
  while (!fh)
    if ((fh = open(opielockfilename, O_WRONLY | O_CREAT | O_EXCL, 0600)) <= 0) {
      if ((fh = open(opielockfilename, O_RDWR, 0600)) <= 0)
        goto lockret;
      if ((i = read(fh, buffer, sizeof(buffer))) <= 0)
        goto lockret;

      buffer[sizeof(buffer) - 1] = 0;
      buffer[i - 1] = 0;

      if (!(c = strchr(buffer, '\n')))
        break;

      *(c++) = 0;

      if (!(c2 = strchr(c, '\n')))
        break;

      *(c2++) = 0;

      if (!(pid = atoi(buffer)))
        break;

      if (!(t = atoi(c)))
        break;

      if ((time(NULL) + OPIE_LOCK_TIMEOUT) < t)
        break;

      if (kill(pid, 0))
        break;

      close(fh);
      fh = 0;
      sleep(1);
      if (waits++ > 3) {
        rval = 1; 
        goto lockret;
      };
    };

  sprintf(buffer, "%d\n%d\n", getpid(), time(NULL));
  i = strlen(buffer) + 1;
  if (lseek(fh, 0, SEEK_SET)) { 
    close(fh);
    unlink(opielockfilename);
    fh = 0;
    goto lockret;
  };
  if (write(fh, buffer, i) != i) {
    close(fh);
    unlink(opielockfilename);
    fh = 0;
    goto lockret;
  };
  close(fh);
  if (!(fh = open(opielockfilename, O_RDWR, 0600))) {
    unlink(opielockfilename);
    goto lockret;
  };
  if (read(fh, buffer2, i) != i) {
    close(fh);
    unlink(opielockfilename);
    fh = 0;
    goto lockret;
  };
  close(fh);
  if (memcmp(buffer, buffer2, i)) {
    unlink(opielockfilename);
    goto lockret;
  };
    
  rval = 0;
  atexit(opieunlockaeh);

lockret:
  if (fh)
    close(fh);
  return rval;
#else /* USER_LOCKING */
  return 0;
#endif /* USER_LOCKING */
}

/* 
  Just remove the lock, right?
  Well, not exactly -- we need to make sure it's ours. 
*/
static int opieunlock()
{
#ifdef USER_LOCKING
  int fh, rval = -1, pid, t, i;
  char buffer[128], *c;

  if (!opielockfilename)
    return -1;

  if (!(fh = open(opielockfilename, O_RDWR, 0600)))
    goto unlockret;

  if (!(i = read(fh, buffer, sizeof(buffer))))
    goto unlockret;

  buffer[sizeof(buffer) - 1] = 0;
  buffer[i - 1] = 0;

  if (!(c = strchr(buffer, '\n')))
    goto unlockret;

  *(c++) = 0;

  if (!(pid = atoi(buffer)))
    goto unlockret;

  if ((pid != getpid()) && (time(NULL) + OPIE_LOCK_TIMEOUT <= t) && 
    (!kill(pid, 0))) { 
    rval = 1;
    goto unlockret1;
  }

  rval = 0;

unlockret:
  unlink(opielockfilename);

unlockret1:
  if (fh)
    close(fh);
  free(opielockfilename);
  opielockfilename = NULL;
  return rval;
#else /* USER_LOCKING */
  return 0;
#endif /* USER_LOCKING */
}

/* Generate a random challenge */
/* This could grow into quite a monster, really. Random is good enough for
   most situations; it is certainly better than a fixed string */
static void opierandomchallenge(prompt)
char *prompt;
{
  time_t now;

  char buf[MAXHOSTNAMELEN];

  time(&now);
  srand(now);
  now = rand();
  if (gethostname(buf, sizeof(buf)) < 0) {
    buf[0] = 'k';
    buf[1] = 'e';
  }
#ifdef DEBUG
  fprintf(stderr, "hostname is %s\n", buf);
#endif	/* DEBUG */
  buf[NAMELEN] = 0;
  sprintf(prompt, "otp-md%d %d %s%04d", MDX, (rand() % 499) + 1, buf,
	  (now % 9998) + 1);
}

/* Return an OTP challenge string for user 'name'. 

   The return values are:

   0  = All good
   -1 = Low-level error (file, memory, I/O, etc.)
   1  = High-level error (user not found or locked)

   This function MUST eventually be followed by an opieverify() to release
   the user lock and file handles.

   This function will give you a blanked-out state block if it returns a
   nonzero status. Even though it returns a non-zero status and a blank
   state block, you still MUST call opieverify() to clear the lock and
   any internal state (the latter condition is not actually used yet).
*/
int opiechallenge(mp, name, ss)
struct opie *mp;
char *name;
char *ss;
{
  int rval = -1;

  memset(mp, 0, sizeof(*mp));

  rval = opielookup(mp, name);

  if (!rval)
    rval = opielock(name);

  if (rval) {
    opierandomchallenge(ss);
    memset(mp, 0, sizeof(*mp));
  } else
    sprintf(ss, "otp-md%d %d %s", MDX, mp->n - 1, mp->seed);

  return rval;
}

/* Find an entry in the One-time Password database.
 * Return codes:
 * -1: error in opening database
 *  0: entry found, file R/W pointer positioned at beginning of record
 *  1: entry not found, file CLOSED
 *  2: entry found, file R/O pointer positioned at beginning of record
       (i.e., could not get write access)
 */
int opielookup(mp, name)
struct opie *mp;
char *name;
{
  int found;
  int len;
  long recstart;
  char *cp;
  struct stat statbuf;
  int rval = 0;

  /* See if the KEY_FILE exists, and create it if not */
  if (stat(KEY_FILE, &statbuf) == -1 && errno == ENOENT) {
    mp->keyfile = fopen(KEY_FILE, "w+");
  } else {
    /* Otherwise open normally for update, if we can */
    if (!(mp->keyfile = fopen(KEY_FILE, "r+")) && (errno == EACCES)) {
	mp->keyfile = fopen(KEY_FILE, "r");
	rval = 2;
    }
  }

  if (mp->keyfile == NULL)
    return -1;

  /* Look up user name in database */
  len = strlen(name);
  if (len > OPIE_PRINCIPAL_MAX)
    len = OPIE_PRINCIPAL_MAX;	/* Added 8/2/91  -  nmh */
  found = 0;

  while (!feof(mp->keyfile)) {
    recstart = ftell(mp->keyfile);
    mp->recstart = recstart;
    if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
      break;

    opiestrip_crlf(mp->buf);

    if (mp->buf[0] == '#')
      continue;	/* Must be comment line */
    if ((mp->logname = strtok(mp->buf, " \t")) == NULL)
      continue;
    if ((cp = strtok(NULL, " \t")) == NULL)
      continue;
    mp->n = atoi(cp);
    if ((mp->seed = strtok(NULL, " \t")) == NULL)
      continue;
    if ((mp->val = strtok(NULL, " \t")) == NULL)
      continue;
    if (strlen(mp->logname) == len &&
	strncmp(mp->logname, name, len) == 0) {
      found = 1;
      break;
    }
  }
  if (found) {
    fseek(mp->keyfile, recstart, 0);
    return rval;
  } else {
    fclose(mp->keyfile);
    mp->keyfile = NULL;
    return 1;
  };
}

/* Verify response to an opie challenge.

   Return codes:
   -1: Error of some sort; database unchanged
    0:  Verify successful, database updated
    1:  Verify failed, database unchanged
  
   The database file is always closed by this call.

   This function MUST be called exactly once in a pair with calls to 
   opiechallenge() in order to set and clear locks properly.

   This function always clears the internal state block. N.B. that the 
   Bellcore S/Key Version 1 software distribution looks inside the internal
   state block to find the current sequence number and do appropriate
   warnings. This interface should not be used with OPIE and will not be
   supported in the future. Use opiegetsequence() instead.
*/
int opieverify(mp, response)
struct opie *mp;
char *response;
{
  char key[8];
  char fkey[8];
  char filekey[8];
  time_t now;
  struct tm *tm;
  char tbuf[27];
  int rval = -1;
  char *cp;

  if (!mp->keyfile)
    goto invalid;

  time(&now);
  tm = localtime(&now);
  strftime(tbuf, sizeof(tbuf), " %b %d,%Y %T", tm);

  if (response == NULL)
    goto invalid;

  opiestrip_crlf(response);

  /* Convert response to binary */
  if ((opieetob(key, response) != 1) && (opieatob8(key, response) != 0))
    goto invalid;

  /* Compute fkey = opiehash(key, algorithm) */
  memcpy(fkey, key, sizeof(key));
  opiehash(fkey, MDX);
  /* In order to make the window of update as short as possible we must do
     the comparison here and if OK write it back otherwise the same password
     can be used twice to get in to the system. */

#if HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H
  setpriority(PRIO_PROCESS, 0, -4);	/* present only in BSD */
#endif /* HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H */

#if HAVE_FPURGE
  if (fpurge(mp->keyfile))
    goto invalid;
#endif /* HAVE_FPURGE */
  /* reread the file record NOW */
  if (fseek(mp->keyfile, mp->recstart, 0))
    goto invalid;
  if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
    goto invalid;
  opiestrip_crlf(mp->buf);
  if (!(mp->logname = strtok(mp->buf, " \t")))
    goto invalid;
  {
    int n;

    if (!(cp = strtok(NULL, " \t")))
      goto invalid;
    if (!(n = atoi(cp)))
      goto invalid;
    if (mp->n != n) {
      rval = 1;
      goto invalid;
    }
  }
  if (!(mp->seed = strtok(NULL, " \t")))
    goto invalid;
  if (!(mp->val = strtok(NULL, " \t")))
    goto invalid;
  /* And convert file value to hex for comparison */
  opieatob8(filekey, mp->val);

  /* Do actual comparison */
  if (memcmp(filekey, fkey, 8) != 0) {
    rval = 1;
    goto invalid;
  }

  /* Update key in database by overwriting entire record. Note that we must
     write exactly the same number of bytes as in the original record (note
     fixed width field for N). */
  opiebtoa8(mp->val, key);
  mp->n--;
  fseek(mp->keyfile, mp->recstart, 0);
  fprintf(mp->keyfile, "%s %04d %-16s %s %-21s\n", mp->logname, mp->n, mp->seed,
	  mp->val, tbuf);

  rval = 0;

invalid:
  if (mp->keyfile)
    fclose(mp->keyfile);

#if HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H
  setpriority(PRIO_PROCESS, 0, 0);
#endif /* HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H */

  opieunlock();

  memset(mp, 0, sizeof(*mp));

  return rval;
}

int opiegetsequence(stateblock)
struct opie *stateblock;
{
  return stateblock->n;
}

/* Convert 8-byte hex-ascii string to binary array
 * Returns 0 on success, -1 on error
 */
int opieatob8(out, in)
char *out, *in;
{
  register int i;
  register int val;

  if (in == NULL || out == NULL)
    return -1;

  for (i = 0; i < 8; i++) {
    if ((in = opieskipspace(in)) == NULL)
      return -1;
    if ((val = opiehtoi(*in++)) == -1)
      return -1;
    *out = val << 4;

    if ((in = opieskipspace(in)) == NULL)
      return -1;
    if ((val = opiehtoi(*in++)) == -1)
      return -1;
    *out++ |= val;
  }
  return 0;
}
