/* Copyright (c) 1997, 1998, 1999 Thorsten Kukuk
   Author: Thorsten Kukuk <kukuk@suse.de>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

#if defined(HAVE_CONFIG_H)
#include "config.h"
#endif

#define _GNU_SOURCE

#include <pwd.h>
#include <ctype.h>
#include <locale.h>
#include <libintl.h>
#include <unistd.h>
#include <string.h>
#include <rpc/key_prot.h>
#include <rpc/des_crypt.h>
#include <rpcsvc/nis.h>
#include "nispasswd.h"

extern int key_get_conv (char *pkey, des_block *deskey);

#ifndef _
#define _(String) gettext (String)
#endif

static const char *
npderr2str (nispasswd_code error)
{
  switch (error)
    {
    case NPD_NOTMASTER:
      return _("Server is not master of this domain");
    case NPD_NOSUCHENTRY:
      return _("No passwd entry exists for this user");
    case NPD_IDENTINVALID:
      return _("Identifier invalid");
    case NPD_NOPASSWD:
      return _("No password stored");
    case NPD_NOSHDWINFO:
      return _("No shadow information stored");
    case NPD_SHDWCORRUPT:
      return _("Shadow information corrupted");
    case NPD_NOTAGED:
      return _("Passwd has not aged sufficiently");
    case NPD_CKGENFAILED:
      return _("Common key could not be generated");
    case NPD_VERFINVALID:
      return _("Verifier mismatch");
    case NPD_PASSINVALID:
      return _("All auth attempts incorrect");
    case NPD_ENCRYPTFAIL:
      return _("Encryption failed");
    case NPD_DECRYPTFAIL:
      return _("Decryption failed");
    case NPD_KEYSUPDATED:
      return _("New key-pair generated for user");
    case NPD_KEYNOTREENC:
      return _("Could not reencrypt secret key");
    case NPD_PERMDENIED:
      return _("Permission denied");
    case NPD_SRVNOTRESP:
      return _("Server not responding");
    case NPD_NISERROR:
      return _("NIS+ server error");
    case NPD_SYSTEMERR:
      return _("System error");
    case NPD_BUFTOOSMALL:
      return _("Buffer too small");
    case NPD_INVALIDARGS:
      return _("Invalid args to function");
    default:
      return _("Unknown error!");
    }
}

/* prompt - ask the user for a given field and return it,
   allows the user to use a default value */
static char *
prompt (const char *question, const char *default_value)
{
  int len;
  char *ans;
  static char buf[FILENAME_MAX];

  if (!default_value)
    default_value = "";
  printf ("%s [%s]: ", question, default_value);
  buf[0] = '\0';
  if (fgets (buf, sizeof (buf), stdin) == NULL)
    {
      fputs (_("\nAborted.\n"), stderr);
      exit (1);
    }
  /* remove the newline at the end of buf. */
  ans = buf;
  while (isspace (*ans))
    ++ans;
  len = strlen (ans);
  while (len > 0 && isspace (ans[len - 1]))
    --len;
  if (len <= 0)
    return strdup (default_value);
  ans[len] = 0;
  return strdup (buf);
}

static int
verifypassword (const char *oldpwd, const char *pwdstr, const char *user,
		uid_t gotuid)
{
  /* this function will verify the user's password
     for some silly things.
     return values: 0 = not ok, 1 = ok */

  const char *p, *q;
  int ucase, lcase, other, r;

  if ((strlen (pwdstr) < 6) && gotuid)
    {
      fputs (_("The password must have at least 6 characters.\n"), stdout);
      return 0;
    }

  other = ucase = lcase = 0;
  for (p = pwdstr; *p; ++p)
    {
      ucase = ucase || isupper (*p);
      lcase = lcase || islower (*p);
      other = other || !isalpha (*p);
    }

  if ((!ucase || !lcase) && !other && gotuid)
    {
      fputs (_("The password must have both upper and lowercase "), stdout);
      fputs (_("letters, or non-letters.\n"), stdout);
      return 0;
    }

  if (oldpwd && !strncmp (oldpwd, pwdstr, 8) && gotuid)
    {
      fputs (_("You cannot reuse the old password.\n"), stdout);
      return 0;
    }
  r = 0;
  for (p = pwdstr, q = user; *q && *p; q++, p++)
    {
      if (tolower (*p) != tolower (*q))
	{
	  r = 1;
	  break;
	}
    }

  for (p = pwdstr + strlen (pwdstr) - 1, q = user;
       *q && p >= pwdstr; q++, p--)
    {
      if (tolower (*p) != tolower (*q))
	{
	  r += 2;
	  break;
	}
    }

  if (gotuid && r != 3)
    {
      fputs (_("Please don't use something like your username as password.\n"),
	     stdout);
      return 0;
    }
  return 1;			/* OK */
}

/* If is_admin is set, we only warn ones that the password is bad */
static char *
getnewpwd (const char *oldpwd, const char *user, int is_admin)
{
  char *pwdstr = NULL;
  char pwdstr1[10];
  uid_t gotuid;
  int try = 0;

  gotuid = getuid ();

redo_it:
  ++try;
  if (try > 3)
    exit (3);
  pwdstr = getpass (_("Enter new password: "));
  if (pwdstr[0] == '\0' && gotuid != 0)
    {
      fputs (_("Password not changed.\n"), stdout);
      exit (3);
    }

  if (!(is_admin && try > 1))
    if (verifypassword (oldpwd, pwdstr, user, gotuid) != 1)
      goto redo_it;

  strncpy (pwdstr1, pwdstr, 9);
  pwdstr1[9] = '\0';
  pwdstr = getpass (_("Re-enter new password: "));

  if (strncmp (pwdstr, pwdstr1, 8))
    {
      fputs ("You misspelled it. Password not changed.\n", stdout);
      goto redo_it;
    }

  return pwdstr;
}

#define NISENTRYVAL(col,obj) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_value.ec_value_val)
#define NISENTRYLEN(col,obj) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_value.ec_value_len)
#define NISENTRYFLAG(col,res) \
        ((obj)->EN_data.en_cols.en_cols_val[(col)].ec_flags)

static int
update_npd (nis_object *obj, int change_gecos, int change_shell)
{
  nis_server **server;
  CLIENT *clnt;
  struct timeval timeout;
  char oldpwd[17];
  char *oldclearpwd;
  npd_request request;
  npd_update update;
  nispasswd_authresult result;
  nispasswd_updresult updresult;
  char pkey_host[HEXKEYBYTES + 1];
  char pkey_user[HEXKEYBYTES + 1];
  char skey_data[HEXKEYBYTES + 1];
  char usernetname[MAXNETNAMELEN + 1], servernetname[MAXNETNAMELEN + 1];
  des_block CK;
  const char *masterhost;
  des_block cryptbuf;
  char ivec[8];
  long *ixdr;
  int error;
  char *cp;

  /* build netname for user or if caller == root, host */
  if (getuid () == 0 && strncmp (NISENTRYVAL(0,obj), "root",
				 NISENTRYLEN(0,obj)) == 0)
    {
      char hostname[MAXHOSTNAMELEN + 1];

      if (gethostname (hostname, MAXHOSTNAMELEN) != 0)
	{
	  fputs (_("Could not determine hostname !\n"), stderr);
	  return 3;
	}
      host2netname (usernetname, hostname, NULL);
    }
  else
    user2netname (usernetname, getuid (), NULL);

  /* get old password for decrypting secret key and further use. */
  memset (oldpwd, '\0', sizeof (oldpwd));
  strncpy (oldpwd, getpass (_("Enter old NIS+ password: ")), 11);
  oldclearpwd = strdup (oldpwd);
  if (!getsecretkey (usernetname, skey_data, oldpwd))
    {
      if (!getsecretkey (usernetname, skey_data,
			 getpass (_("Enter RPC secure password: "))))
	{
	  fprintf (stderr, _("Can't find %s's secret key\n"), usernetname);
	  return 3;
	}
    }

  /* fill in request struct */
  memset (&request, '\0', sizeof (request));
  request.ident = 0;
  request.key_type = strdup ("DES");
  request.domain = strdup (nis_domain_of (obj->zo_domain));
  request.username = strndup (NISENTRYVAL(0,obj), NISENTRYLEN(0,obj));

  /* get publickey of the user */
  memset (pkey_user, '\0', sizeof (pkey_user));
  if (getpublickey (usernetname, pkey_user) == 0 || pkey_user[0] == '\0')
    {
      fprintf (stderr, _("Could not get public key for %s!\n"), usernetname);
      return 3;
    }
  request.user_pub_key.user_pub_key_len = HEXKEYBYTES;
  request.user_pub_key.user_pub_key_val = pkey_user;
  /* get publickey of the server running rpc.nispasswdd. For that,
     we have to find the name of the master server for the user domain. */
  server = nis_getservlist (request.domain);
  if (server[0] == NULL)
    {
      fputs (_("Could not determine the NIS+ root server!\n"), stderr);
      return 3;
    }
  masterhost = strdup (server[0]->name);
  cp = strchr (masterhost, '.');
  *cp = '\0';
  nis_freeservlist (server);
  host2netname (servernetname, masterhost, NULL);
  memset (pkey_host, '\0', sizeof (pkey_host));
  if (getpublickey (servernetname, pkey_host) == 0 || pkey_host[0] == '\0')
    {
      fprintf (stderr, _("Could not get public key for %s!\n"), servernetname);
      return 3;
    }

  /* Generate common DES key from public server key and secret user key */
  if (key_get_conv (pkey_host, &CK) != 0)
    {
      fputs (_("Could not create conversion key !\n"), stderr);
      return 3;
    }

  /* encrypt old clear password. Don't know why Sun needs 16 byte,
     normaly a password could only be 8 byte. And later in the protocol
     SUN allows only 12 byte. */
  memset (ivec, 0, 8);
  error = cbc_crypt ((char *) &CK, oldpwd,
		   16, DES_ENCRYPT | DES_HW, ivec);
  if (DES_FAILED (error))
    {
      fputs (_("DES encryption failure"), stderr);
      return 3;
    }
  request.npd_authpass.npd_authpass_len = 16;
  request.npd_authpass.npd_authpass_val = oldpwd;

  /* Try to authenticate us and the server.
     XXX This should be done in a loop,
     since the server could be bussy or the password wrong */
  clnt = clnt_create (masterhost, NISPASSWD_PROG, NISPASSWD_VERS, "tcp");
  if (clnt == NULL)
    {
      fprintf (stderr, _("rpc.nispasswd not running on %s ?\n"), masterhost);
      return 3;
    }
  memset ((char *) &result, 0, sizeof (result));
  timeout.tv_sec = 25;
  timeout.tv_usec = 0;
  error = clnt_call (clnt, NISPASSWD_AUTHENTICATE,
		     (xdrproc_t) xdr_npd_request, (char *) &request,
		     (xdrproc_t) xdr_nispasswd_authresult, (char *) &result,
		     timeout);

  if (error)
    {
      clnt_perrno (error);
      fputs ("\n", stderr);
      return 3;
    }

  if (result.status != NPD_SUCCESS)
    {
      if (result.status == NPD_TRYAGAIN)
	fputs (_("ERROR: password incorrect, try again\n"), stderr);
      else
	{
	  fprintf (stderr, _("ERROR: %s\n"),
		   npderr2str (result.nispasswd_authresult_u.npd_err));
	  fputs (_("       password not changed\n"), stderr);
	}
      return 3;
    }

  /* Decrypt the ID and the random value. Not easy, since Sparc's are
     big endian, i?86 little endian, we have to revert to big endian
     for decrypt */
  memset (&cryptbuf, '\0', sizeof (cryptbuf));
  ixdr = (long *) &cryptbuf;
  IXDR_PUT_U_INT32 (ixdr, result.nispasswd_authresult_u.npd_verf.npd_xid);
  IXDR_PUT_U_INT32 (ixdr, result.nispasswd_authresult_u.npd_verf.npd_xrandval);
  error = ecb_crypt ((char *) &CK, (char *) &cryptbuf, 8,
		     DES_DECRYPT | DES_HW);
  if (DES_FAILED (error))
    {
      fputs (_("DES decryption failure!\n"), stderr);
      return 3;
    }

  /* fill out update request */
  memset (&update, 0, sizeof (update));
  update.ident = ntohl (cryptbuf.key.high);
  update.xnewpass.npd_xrandval = cryptbuf.key.low;
  memset (update.xnewpass.pass, '\0', __NPD_MAXPASSBYTES);
  /* Don't ask for new password, if we only wish to change gecos or shell */
  if (!change_gecos && !change_shell)
    strncpy (update.xnewpass.pass, getnewpwd (oldpwd, request.username, 0),
	     __NPD_MAXPASSBYTES);
  else
    strncpy (update.xnewpass.pass, oldclearpwd, __NPD_MAXPASSBYTES);

  cp = strndup (NISENTRYVAL(4,obj), NISENTRYLEN(4,obj));
  update.pass_info.pw_gecos = "";
  if (change_gecos)
    {
      char *gecos = prompt (_("Full name"), cp);

      if (gecos != NULL)
	update.pass_info.pw_gecos = gecos;
    }
  free (cp);

  cp = strndup (NISENTRYVAL(6,obj), NISENTRYLEN(6,obj));
  update.pass_info.pw_shell = "";
  if (change_shell)
    {
      char *shell = prompt (_("New shell"), cp);

      if (shell != NULL)
	update.pass_info.pw_shell = shell;
    }
  free (cp);

  memset (ivec, 0, 8);
  error = cbc_crypt ((char *) &CK, (char *) &update.xnewpass,
		     16, DES_ENCRYPT | DES_HW, ivec);
  if (DES_FAILED (error))
    {
      fputs (_("DES decryption failure!\n"), stderr);
      return 3;
    }

  /* update.xnewpass.npd_xrandval will be changed in XDR again */
  update.xnewpass.npd_xrandval = ntohl (update.xnewpass.npd_xrandval);


  memset ((char *) &updresult, 0, sizeof (updresult));
  timeout.tv_sec = 25;
  timeout.tv_usec = 0;
  error = clnt_call (clnt, NISPASSWD_UPDATE, (xdrproc_t) xdr_npd_update,
		     (caddr_t) &update, (xdrproc_t) xdr_nispasswd_updresult,
		     (caddr_t) &updresult, timeout);
  if (error)
    {
      clnt_perrno (error);
      fprintf (stderr, "\n");
      return 3;
    }
  clnt_destroy (clnt);
  if (updresult.status != NPD_SUCCESS)
    {
      if (updresult.status == NPD_FAILED)
	{
	  fprintf (stderr, _("ERROR: %s\n"),
		   npderr2str (updresult.nispasswd_updresult_u.npd_err));
	  fputs (_("       password not changed\n"), stderr);
	}
      else if (updresult.status == NPD_PARTIALSUCCESS)
	{
	  nispasswd_error *err;
	  fputs (_("ERROR: Only partial success\n"), stderr);
	  err = &updresult.nispasswd_updresult_u.reason;
	  while (err != NULL)
	    {
	      switch (err->npd_field)
		{
		case NPD_PASSWD:
		  fprintf (stderr, _("\tpassword field: %s\n"),
			   npderr2str (err->npd_code));
		  break;
		case NPD_GECOS:
		  fprintf (stderr, _("\tgecos field: %s\n"),
			   npderr2str (err->npd_code));
		case NPD_SHELL:
		  fprintf (stderr, _("\tshell field: %s\n"),
			   npderr2str (err->npd_code));
		  break;
		case NPD_SECRETKEY:
		  fprintf (stderr, _("\tsecret key: %s\n"),
			   npderr2str (err->npd_code));
		  break;
		}
	      err = err->next;
	    }
	}
      else
	{
	  fputs (_("ERROR: Unknown error, don't know what happend\n"),
		 stderr);
	}
      return 3;
    }

  fprintf (stdout, _("NIS+ password information changed for %s\n"),
	   request.username);
  fprintf (stdout, _("NIS+ credential information changed for %s\n"),
	   request.username);

  nis_free_object (obj);
  return 0;
}


/* Update password in NIS+ passwd table, do not use rpc.nispasswdd */
static int
update_nisd (nis_object *obj, int change_gecos, int change_shell)
{
  char buf[strlen (obj->zo_name) + strlen (obj->zo_domain) + 10];
  nis_result *result;
  char oldpwd[17];
  char salt[2];
  time_t tm;
  char *cp;
  char *user = strndup (NISENTRYVAL(0,obj), NISENTRYLEN(0,obj));

  /* Don't ask for new password, if we only wish to change gecos or shell */
  if (!change_gecos && !change_shell)
    {
      strncpy (oldpwd, NISENTRYVAL(1,obj), NISENTRYLEN(1,obj));
      oldpwd[NISENTRYLEN(1,obj)] = '\0';
      free (NISENTRYVAL(1,obj));
      time (&tm);
#define bin_to_ascii(c) ((c)>=38?((c)-38+'a'):(c)>=12?((c)-12+'A'):(c)+'.')
      salt[0] = bin_to_ascii (tm & 0x3f);
      salt[1] = bin_to_ascii ((tm >> 6) & 0x3f);
      NISENTRYVAL(1,obj) = strdup (crypt (getnewpwd (oldpwd, user, 1), salt));
      NISENTRYLEN(1,obj) = strlen (NISENTRYVAL(1,obj));
      NISENTRYFLAG(1,obj) = NISENTRYFLAG(1,obj) | EN_MODIFIED;
    }

  cp = strndup (NISENTRYVAL(4,obj), NISENTRYLEN(4,obj));
  free (NISENTRYVAL(4,obj));
  if (change_gecos)
    {
      char *gecos = prompt (_("Full name"), cp);

      if (gecos != NULL)
	{
	  NISENTRYVAL(4,obj) = gecos;
	  NISENTRYFLAG(4,obj) = NISENTRYFLAG(4,obj) | EN_MODIFIED;
	}
      else
        NISENTRYVAL(4,obj) = cp;
    }
  else
    NISENTRYVAL(4,obj) = cp;
  NISENTRYLEN(4,obj) = strlen (NISENTRYVAL(4,obj));

  cp = strndup (NISENTRYVAL(6,obj), NISENTRYLEN(6,obj));
  free (NISENTRYVAL(6,obj));
  if (change_shell)
    {
      char *shell = prompt (_("New shell"), cp);

      if (shell != NULL)
	{
	  NISENTRYVAL(6,obj) = shell;
	  NISENTRYFLAG(6,obj) = NISENTRYFLAG(6,obj) | EN_MODIFIED;
	}
      else
        NISENTRYVAL(6,obj) = cp;
    }
  else
    NISENTRYVAL(6,obj) = cp;
  NISENTRYLEN(6,obj) = strlen (NISENTRYVAL(6,obj));

  sprintf (buf, "%s.%s", obj->zo_name, obj->zo_domain);
  result = nis_modify_entry (buf, obj, 0);
  if (result->status != NIS_SUCCESS)
    {
      fprintf (stderr, _("nispasswd: Password information update failed\n"));
      fprintf (stderr,   "           %s\n", nis_sperrno (result->status));
      nis_freeresult (result);
      return 1;
    }
  else
    nis_freeresult (result);

  fprintf (stdout,
	   _("The NIS+ credential information for %s will not be changed.\n"),
	   user);
  fprintf (stdout,
	   _("User %s must do the following to update his/her\n"), user);
  fprintf (stdout, _("credential information:\n"));
  fprintf (stdout, _("Use NEW password for login and OLD password for keylogin.\n"));
  fprintf (stdout, _("Use \"chkey -p\" to reencrypt the credentials with the\n"));
  fprintf (stdout, _("new login passwd.\n"));
  fprintf (stdout, _("The user must keylogin explicitly after their next login.\n"));

  nis_free_object (obj);
  return 0;
}

int
npd_upd_pwd (char *domainname, char *user, int change_gecos, int change_shell)
{
  nis_result *result;
  nis_object *obj;
  char domain[NIS_MAXNAMELEN + 1];
  char *buf = NULL;
  char *group;

  if (strlen (domainname) == 0)
    strncpy (domain, nis_local_directory (), NIS_MAXNAMELEN);
  else
    strncpy (domain, domainname, NIS_MAXNAMELEN);
  domain[NIS_MAXNAMELEN] = '\0';

  /* Get group of passwd table */
  if (user != NULL)
    buf = alloca (strlen (user) + strlen (domain) + 30);
  else
    buf = alloca (strlen (domain) + 30);
  sprintf (buf, "passwd.org_dir.%s", domain);
  result = nis_lookup (buf, 0);
  if (result->status != NIS_SUCCESS)
    {
      fprintf (stderr, _("NIS+ passwd table not found:\n"),
	       nis_sperrno (result->status));
      return 4;
    }

  group = strdup (NIS_RES_OBJECT(result)->zo_group);
  nis_freeresult (result);

  /* Get old NIS+ passwd information for caller or parameter. */
  if (user != NULL)
    sprintf (buf, "[name=%s],passwd.org_dir.%s", user, domain);
  else
    sprintf (buf, "[uid=%ld],passwd.org_dir.%s", (long)getuid (),
	     domain);

  result = nis_list (buf, 0, NULL, NULL);

  if (result->status != NIS_SUCCESS)
    {
      fputs (_("User not found in NIS+ table.\n"), stderr);
      return 4;
    }

  obj = nis_clone_object (result->objects.objects_val, NULL);
  nis_freeresult (result);

  if (nis_ismember (nis_local_principal (), group))
    return update_nisd (obj, change_gecos, change_shell);
  else
    return update_npd (obj, change_gecos, change_shell);
}
