/* opiesu.c: main body of code for the su(1m) program

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. Added struct group declaration.
	        Added Solaris(+others?) sulog capability. Symbol changes
		for autoconf. Removed des_crypt.h. File renamed to
		opiesu.c. Symbol+misc changes for autoconf. Added bletch
		for setpriority.
        Modified at NRL for OPIE 2.02. Added SU_STAR_CHECK (turning a bug
                into a feature ;). Fixed Solaris shadow password problem
                introduced in OPIE 2.01 (the shadow password structure is
                spwd, not spasswd).
        Modified at NRL for OPIE 2.01. Changed password lookup handling
                to use a static structure to avoid problems with drain-
                bamaged shadow password packages. Always log failures.
                Make sure to close syslog by function to avoid problems 
                with drain bamaged syslog implementations. Log a few 
                interesting errors.
	Modified at NRL for OPIE 2.0.
	Modified at Bellcore for the S/Key Version 1 software distribution.
	Originally from BSD.
*/

/*
 * Copyright (c) 1980 Regents of the University of California.
 * All rights reserved.  The Berkeley software License Agreement
 * specifies the terms and conditions for redistribution.
 */

#ifndef lint
char copyright[] =
"@(#) Copyright (c) 1980 Regents of the University of California.\n\
 All rights reserved.\n";
#endif	/* not lint */

#ifndef lint
static char sccsid[] = "@(#)su.c	5.5 (Berkeley) 1/18/87";
#endif	/* not lint */

#include "opie_cfg.h"

#include <stdio.h>
#include <pwd.h>
#include <grp.h>
#include <syslog.h>
#include <sys/types.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 */
#include <stdlib.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#include "opie.h"

char userbuf[16] = "USER=";
char homebuf[128] = "HOME=";
char shellbuf[128] = "SHELL=";
char pathbuf[128] = "PATH=";
char *cleanenv[] =
{userbuf, homebuf, shellbuf, pathbuf, 0, 0};
char *user = "root";
char *shell = "/bin/sh";
int fulllogin;
int fastlogin;

extern char **environ;
struct passwd thisuser, nouser;

#if HAVE_SHADOW_H
#include <shadow.h>
#endif /* HAVE_SHADOW_H */

#if HAVE_CRYPT_H
#include <crypt.h>
#endif /* HAVE_CRYPT_H */

void closelog __ARGS((void));

/* We allow the malloc()s to potentially leak data out because we can
only call this routine about four times in the lifetime of this process
and the kernel will free all heap memory when we exit or exec. */
int lookupuser(name)
char *name;
{
  struct passwd *pwd;
#if HAVE_SHADOW
  struct spwd *spwd;
#endif /* HAVE_SHADOW */

  memcpy(&thisuser, &nouser, sizeof(thisuser));

  if (!(pwd = getpwnam(name)))
    return -1;

  thisuser.pw_uid = pwd->pw_uid;
  thisuser.pw_gid = pwd->pw_gid;

  if (!(thisuser.pw_name = malloc(strlen(pwd->pw_name) + 1)))
    goto lookupuserbad;
  strcpy(thisuser.pw_name, pwd->pw_name);

  if (!(thisuser.pw_dir = malloc(strlen(pwd->pw_dir) + 1)))
    goto lookupuserbad;
  strcpy(thisuser.pw_dir, pwd->pw_dir);

  if (!(thisuser.pw_shell = malloc(strlen(pwd->pw_shell) + 1)))
    goto lookupuserbad;
  strcpy(thisuser.pw_shell, pwd->pw_shell);

#if HAVE_SHADOW
  if (!(spwd = getspnam(name)))
	goto lookupuserbad;

  pwd->pw_passwd = spwd->sp_pwdp;

  endspent();
#endif /* HAVE_SHADOW */

  if (!(thisuser.pw_passwd = malloc(strlen(pwd->pw_passwd) + 1)))
    goto lookupuserbad;
  strcpy(thisuser.pw_passwd, pwd->pw_passwd);

  endpwent();

#if SU_STAR_CHECK
  return ((thisuser.pw_passwd[0] == '*') || (thisuser.pw_passwd[0] == '#'));
#else /* SU_STAR_CHECK */
  return 0;
#endif /* SU_STAR_CHECK */

lookupuserbad:
  memcpy(&thisuser, &nouser, sizeof(thisuser));
  return -1;
}

void lsetenv(ename, eval, buf)
char *ename, *eval, *buf;
{
  register char *cp, *dp;
  register char **ep = environ;

  /* this assumes an environment variable "ename" already exists */
  while (dp = *ep++) {
    for (cp = ename; *cp == *dp && *cp; cp++, dp++)
      continue;
    if (*cp == 0 && (*dp == '=' || *dp == 0)) {
      strcat(buf, eval);
      *--ep = buf;
      return;
    }
  }
}

#if DOSULOG
int sulog(status, who)
int status;
char *who;
{
  char *from;
  char *ttynam;
  struct tm *tm;
  FILE *f;

  if (who)
    from = who;
  else
    from = Getlogin();

  if (!strncmp(ttynam = ttyname(2), "/dev/", 5))
    ttynam += 5;

  tm = localtime(NULL);
  
  if (!(f = fopen("/var/adm/sulog", "a"))) {
    fprintf(stderr, "Can't update su log!\n");
    exit(1);
  }

  fprintf(f, "SU %02d/%02d %02d:%02d %c %s %s-%s\n",
	  tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min,
	  result ? '+' : '-', ttynam, from, user);
  fclose(f);
}
#endif /* DOSULOG */

int main(argc, argv)
int argc;
char *argv[];
{
  struct termios attr, orig_attr;
  char buf[1000];
  register char *p;
  struct opie opie;
  int i;
  char pbuf[256];
  char opieprompt[80];
  int fflags, orig_fflags, console = 0;

#define Getlogin()  (((p = getlogin()) && *p) ? p : buf)

  for (i = sysconf(_SC_OPEN_MAX); i > 2; i--)
    close(i);

  strcat(pathbuf, DEFAULT_PATH);

again:
  if (argc > 1 && strcmp(argv[1], "-f") == 0) {
    fastlogin++;
    argc--, argv++;
    goto again;
  }
  if (argc > 1 && strcmp(argv[1], "-c") == 0) {
    console++;
    argc--, argv++;
    goto again;
  }
  if (argc > 1 && strcmp(argv[1], "-") == 0) {
    fulllogin++;
    argc--;
    argv++;
    goto again;
  }
  if (argc > 1 && argv[1][0] != '-') {
    user = argv[1];
    argc--;
    argv++;
  }

  openlog("su", LOG_ODELAY, LOG_AUTH);
  atexit(closelog);

  {
  struct passwd *pwd;

  if ((pwd = getpwuid(getuid())) == NULL) {
    syslog(LOG_CRIT, "'%s' failed for unknown uid %d on %s", argv[0], getuid(), ttyname(2));
#ifdef DOSULOG
    sulog(0, "unknown");
#endif /* DOSULOG */
    exit(1);
  }
  strcpy(buf, pwd->pw_name);
  }

  if (lookupuser(user)) {
    syslog(LOG_CRIT, "'%s' failed for %s on %s", argv[0], Getlogin(), ttyname(2));
#ifdef DOSULOG
    sulog(0, NULL);
#endif /* DOSULOG */
    fprintf(stderr, "Unknown user: %s\n", user);
    exit(1);
  }

/* Implement the BSD "wheel group" su restriction. */
#if DOWHEEL
  /* Only allow those in group zero to su to root? */
  if (thisuser.pw_uid == 0) {
    struct group *gr;
    if ((gr = getgrgid(0)) != NULL) {
      for (i = 0; gr->gr_mem[i] != NULL; i++)
	if (strcmp(buf, gr->gr_mem[i]) == 0)
	  goto userok;
      fprintf(stderr, "You do not have permission to su %s\n", user);
      exit(1);
    }
userok:
    ;
#if HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H
    setpriority(PRIO_PROCESS, 0, -2);
#endif /* HAVE_SETPRIORITY && HAVE_SYS_RESOURCE_H */
  }
#endif	/* DOWHEEL */

  if (!thisuser.pw_passwd[0] || getuid() == 0)
    goto ok;

  if (console) {
    if (!opiealways(thisuser.pw_dir)) {
      fprintf(stderr, "That account requires OTP responses.\n");
      exit(1);
    };
    /* Get user's secret password */
    fprintf(stderr, "Reminder - Only use this method from the console; NEVER from remote. If you\n");
    fprintf(stderr, "are using telnet, xterm, or a dial-in, type ^C now or exit with no password.\n");
    fprintf(stderr, "Then run su without the -c parameter.\n");
    if (opieinsecure()) {
      fprintf(stderr, "Sorry, but you don't seem to be on the console or a secure terminal.\n");
      exit(1);
    };
  } else {
    /* Attempt an OTP challenge */
    i = opiechallenge(&opie, user, opieprompt);
    printf("%s\n", opieprompt);
    printf("(OTP response required)\n");
  };
  printf("%s's password: ", thisuser.pw_name);
  fflush(stdout);

  /* Set normal line editing */
  orig_fflags = fcntl(STDIN_FILENO, F_GETFL, 0);
  memcpy(&fflags, &orig_fflags, sizeof(int));

  fflags &= ~O_NONBLOCK;
  fcntl(STDIN_FILENO, F_SETFL, fflags);

  /* get original terminal attributes and save them */
  if (tcgetattr(STDIN_FILENO, &orig_attr) != 0)
    return (-1);
  memcpy(&attr, &orig_attr, sizeof(struct termios));

  /* enable ERASE and KILL */
#define CONTROL(x) (x - 64)
#ifdef VKILL
#ifdef CKILL
  attr.c_cc[VKILL] = CKILL;
#else	/* CKILL */
  attr.c_cc[VKILL] = CONTROL('U');
#endif	/* CKILL */
#endif	/* VKILL */
#ifdef VERASE
#ifdef CERASE
  attr.c_cc[VERASE] = CERASE;
#else	/* CERASE */
  attr.c_cc[VERASE] = CONTROL('H');
#endif	/* CERASE */
#endif	/* VERASE */

  /* Turn off echoing */
  attr.c_lflag &= ICANON;
  attr.c_lflag |= ISIG;
  attr.c_lflag &= ~ECHO;

  /* push the new terminal configuration */
  if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) != 0)
    return (-1);

  /* Read password */
  fgets(pbuf, sizeof(pbuf), stdin);
  opiestrip_crlf(pbuf);
  if (strlen(pbuf) == 0 && !console) {
    /* Null line entered; turn echoing back on and read again */
    printf(" (echo on)\n%s's password: ", thisuser.pw_name);
    fflush(stdout);

    /* enable echoing & push the new terminal configuration */
    attr.c_lflag |= (ECHO | ECHOE);
    if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) != 0)
      return (-1);
    fgets(pbuf, sizeof(pbuf), stdin);
    opiestrip_crlf(pbuf);
  } else {
    printf("\n");
  }

  /* Restore previous tty modes */
  fcntl(fileno(stdin), F_SETFL, fflags);
  if (tcsetattr(STDIN_FILENO, TCSANOW, &orig_attr) != 0)
    return (-1);

  if (console) {
    /* Try regular password check, if allowed */
    if (!strcmp(crypt(pbuf, thisuser.pw_passwd), thisuser.pw_passwd))
      goto ok;
  } else {
    int i = opiegetsequence(&opie);
    if (!opieverify(&opie, pbuf)) {
      /* OPIE authentication succeeded */
      if (i < 5)
	fprintf(stderr, "Warning: Change %s's OTP secret pass phrase NOW!\n", user);
      else
	if (i < 10)
	  fprintf(stderr, "Warning: Change %s's OTP secret pass phrase soon.\n", user);
      goto ok;
    };
  };
  fprintf(stderr, "Sorry\n");
  syslog(LOG_CRIT, "'%s' failed for %s on %s", argv[0], Getlogin(), ttyname(2));
#ifdef DOSULOG
  sulog(0, NULL);
#endif /* DOSULOG */
  exit(2);

ok:
  syslog(LOG_NOTICE, "'%s' by %s on %s", argv[0], Getlogin(), ttyname(2));
#ifdef DOSULOG
  sulog(1, NULL);
#endif /* DOSULOG */
  
  if (setgid(thisuser.pw_gid) < 0) {
    perror("su: setgid");
    exit(3);
  }
  if (initgroups(user, thisuser.pw_gid)) {
    fprintf(stderr, "su: initgroups failed\n");
    exit(4);
  }
  if (setuid(thisuser.pw_uid) < 0) {
    perror("su: setuid");
    exit(5);
  }
  if (thisuser.pw_shell && *thisuser.pw_shell)
    shell = thisuser.pw_shell;
  if (fulllogin) {
    cleanenv[4] = getenv("TERM");
    environ = cleanenv;
  }
  if (fulllogin || strcmp(user, "root") != 0)
    lsetenv("USER", thisuser.pw_name, userbuf);
  lsetenv("SHELL", shell, shellbuf);
  lsetenv("HOME", thisuser.pw_dir, homebuf);

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

  if (fastlogin) {
    *argv-- = "-f";
    *argv = "su";
  } else
    if (fulllogin) {
      if (chdir(thisuser.pw_dir) < 0) {
	fprintf(stderr, "No directory\n");
	exit(6);
      }
      *argv = "-su";
    } else {
      *argv = "su";
    }

  closelog();
  for (i = sysconf(_SC_OPEN_MAX); i > 2; i--)
    close(i);

  execv(shell, argv);
  fprintf(stderr, "No shell\n");
  exit(7);
}
