http://www.tienza.es/crux/src/www.jwz.org/xscreensaver/xscreensaver-5.05.tar.gz
[xscreensaver] / driver / passwd-pam.c
index f7d8528fd1a8e665caa8945bf7520258383b7e9e..334d12b913c83d8d42d7822099d47b727853a86c 100644 (file)
@@ -1,7 +1,7 @@
 /* passwd-pam.c --- verifying typed passwords with PAM
  * (Pluggable Authentication Modules.)
  * written by Bill Nottingham <notting@redhat.com> (and jwz) for
- * xscreensaver, Copyright (c) 1993-1998 Jamie Zawinski <jwz@jwz.org>
+ * xscreensaver, Copyright (c) 1993-2008 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
  * documentation.  No representations are made about the suitability of this
  * software for any purpose.  It is provided "as is" without express or 
  * implied warranty.
+ *
+ * Some PAM resources:
+ *
+ *    PAM home page:
+ *    http://www.us.kernel.org/pub/linux/libs/pam/
+ *
+ *    PAM FAQ:
+ *    http://www.us.kernel.org/pub/linux/libs/pam/FAQ
+ *
+ *    PAM Application Developers' Guide:
+ *    http://www.us.kernel.org/pub/linux/libs/pam/Linux-PAM-html/Linux-PAM_ADG.html
+ *
+ *    PAM Mailing list archives:
+ *    http://www.linuxhq.com/lnxlists/linux-pam/
+ *
+ *    Compatibility notes, especially between Linux and Solaris:
+ *    http://www.contrib.andrew.cmu.edu/u/shadow/pam.html
+ *
+ *    The Open Group's PAM API documentation:
+ *    http://www.opengroup.org/onlinepubs/8329799/pam_start.htm
  */
 
 #ifdef HAVE_CONFIG_H
@@ -32,9 +52,16 @@ extern char *blurb(void);
 #include <pwd.h>
 #include <grp.h>
 #include <security/pam_appl.h>
+#include <signal.h>
+#include <errno.h>
+#include <X11/Intrinsic.h>
 
 #include <sys/stat.h>
 
+#include "auth.h"
+
+extern sigset_t block_sigchld (void);
+extern void unblock_sigchld (void);
 
 /* blargh */
 #undef  Bool
@@ -47,16 +74,43 @@ extern char *blurb(void);
 #undef countof
 #define countof(x) (sizeof((x))/sizeof(*(x)))
 
+/* Some time between Red Hat 4.2 and 7.0, the words were transposed 
+   in the various PAM_x_CRED macro names.  Yay!
+ */
+#ifndef  PAM_REFRESH_CRED
+# define PAM_REFRESH_CRED PAM_CRED_REFRESH
+#endif
+
 static int pam_conversation (int nmsgs,
                              const struct pam_message **msg,
                              struct pam_response **resp,
                              void *closure);
 
-struct pam_closure {
-  const char *user;
-  const char *typed_passwd;
-  Bool verbose_p;
-};
+void pam_try_unlock(saver_info *si, Bool verbose_p,
+              Bool (*valid_p)(const char *typed_passwd, Bool verbose_p));
+
+Bool pam_priv_init (int argc, char **argv, Bool verbose_p);
+
+#ifdef HAVE_PAM_FAIL_DELAY
+   /* We handle delays ourself.*/
+   /* Don't set this to 0 (Linux bug workaround.) */
+# define PAM_NO_DELAY(pamh) pam_fail_delay ((pamh), 1)
+#else  /* !HAVE_PAM_FAIL_DELAY */
+# define PAM_NO_DELAY(pamh) /* */
+#endif /* !HAVE_PAM_FAIL_DELAY */
+
+
+/* On SunOS 5.6, and on Linux with PAM 0.64, pam_strerror() takes two args.
+   On some other Linux systems with some other version of PAM (e.g.,
+   whichever Debian release comes with a 2.2.5 kernel) it takes one arg.
+   I can't tell which is more "recent" or "correct" behavior, so configure
+   figures out which is in use for us.  Shoot me!
+ */
+#ifdef PAM_STRERROR_TWO_ARGS
+# define PAM_STRERROR(pamh, status) pam_strerror((pamh), (status))
+#else  /* !PAM_STRERROR_TWO_ARGS */
+# define PAM_STRERROR(pamh, status) pam_strerror((status))
+#endif /* !PAM_STRERROR_TWO_ARGS */
 
 
 /* PAM sucks in that there is no way to tell whether a particular service
@@ -105,84 +159,145 @@ struct pam_closure {
  */
 
 
-/* This can be called at any time, and says whether the typed password
-   belongs to either the logged in user (real uid, not effective); or
-   to root.
+/* On SunOS 5.6, the `pam_conv.appdata_ptr' slot seems to be ignored, and
+   the `closure' argument to pc.conv always comes in as random garbage.
+   So we get around this by using a global variable instead.  Shoot me!
+
+   (I've been told this is bug 4092227, and is fixed in Solaris 7.)
+   (I've also been told that it's fixed in Solaris 2.6 by patch 106257-05.)
  */
-Bool
-pam_passwd_valid_p (const char *typed_passwd, Bool verbose_p)
+static void *suns_pam_implementation_blows = 0;
+
+
+/**
+ * This function is the PAM conversation driver. It conducts a full
+ * authentication round by invoking the GUI with various prompts.
+ */
+void
+pam_try_unlock(saver_info *si, Bool verbose_p,
+              Bool (*valid_p)(const char *typed_passwd, Bool verbose_p))
 {
   const char *service = PAM_SERVICE_NAME;
   pam_handle_t *pamh = 0;
   int status = -1;
   struct pam_conv pc;
-  struct pam_closure c;
-  char *user = 0;
-
-  struct passwd *p = getpwuid (getuid ());
-  if (!p) return False;
-
-  user = strdup (p->pw_name);
-
-  c.user = user;
-  c.typed_passwd = typed_passwd;
-  c.verbose_p = verbose_p;
+  sigset_t set;
+  struct timespec timeout;
 
   pc.conv = &pam_conversation;
-  pc.appdata_ptr = (void *) &c;
+  pc.appdata_ptr = (void *) si;
+
+  /* On SunOS 5.6, the `appdata_ptr' slot seems to be ignored, and the
+     `closure' argument to pc.conv always comes in as random garbage. */
+  suns_pam_implementation_blows = (void *) si;
 
 
   /* Initialize PAM.
    */
-  status = pam_start (service, c.user, &pc, &pamh);
+  status = pam_start (service, si->user, &pc, &pamh);
   if (verbose_p)
     fprintf (stderr, "%s: pam_start (\"%s\", \"%s\", ...) ==> %d (%s)\n",
-             blurb(), service, c.user,
-             status, pam_strerror (pamh, status));
+             blurb(), service, si->user,
+             status, PAM_STRERROR (pamh, status));
   if (status != PAM_SUCCESS) goto DONE;
 
-# ifdef HAVE_PAM_FAIL_DELAY
-  pam_fail_delay (pamh, 0);    /* We handle delays ourself. */
-# endif /* HAVE_PAM_FAIL_DELAY */
-
   /* #### We should set PAM_TTY to the display we're using, but we
      don't have that handy from here.  So set it to :0.0, which is a
      good guess (and has the bonus of counting as a "secure tty" as
      far as PAM is concerned...)
    */
   {
-    const char *tty = ":0.0";
-    status = pam_set_item (pamh, PAM_TTY, strdup(tty));
+    char *tty = strdup (":0.0");
+    status = pam_set_item (pamh, PAM_TTY, tty);
     if (verbose_p)
       fprintf (stderr, "%s:   pam_set_item (p, PAM_TTY, \"%s\") ==> %d (%s)\n",
-               blurb(), tty, status, pam_strerror(pamh, status));
+               blurb(), tty, status, PAM_STRERROR(pamh, status));
+    free (tty);
   }
 
   /* Try to authenticate as the current user.
+     We must turn off our SIGCHLD handler for the duration of the call to
+     pam_authenticate(), because in some cases, the underlying PAM code
+     will do this:
+
+        1: fork a setuid subprocess to do some dirty work;
+        2: read a response from that subprocess;
+        3: waitpid(pid, ...) on that subprocess.
+
+    If we (the ignorant parent process) have a SIGCHLD handler, then there's
+    a race condition between steps 2 and 3: if the subprocess exits before
+    waitpid() was called, then our SIGCHLD handler fires, and gets notified
+    of the subprocess death; then PAM's call to waitpid() fails, because the
+    process has already been reaped.
+
+    I consider this a bug in PAM, since the caller should be able to have
+    whatever signal handlers it wants -- the PAM documentation doesn't say
+    "oh by the way, if you use PAM, you can't use SIGCHLD."
    */
-  status = pam_authenticate (pamh, 0);
-  if (verbose_p)
-    fprintf (stderr, "%s:   pam_authenticate (...) ==> %d (%s)\n",
-             blurb(), status, pam_strerror(pamh, status));
-  if (status == PAM_SUCCESS)  /* Win! */
-    goto DONE;
 
-  /* If that didn't work, set the user to root, and try to authenticate again.
-   */
-  c.user = "root";
-  status = pam_set_item (pamh, PAM_USER, strdup(c.user));
+  PAM_NO_DELAY(pamh);
+
   if (verbose_p)
-    fprintf (stderr, "%s:   pam_set_item(p, PAM_USER, \"%s\") ==> %d (%s)\n",
-             blurb(), c.user, status, pam_strerror(pamh, status));
-  if (status != PAM_SUCCESS) goto DONE;
+    fprintf (stderr, "%s:   pam_authenticate (...) ...\n", blurb());
 
+  timeout.tv_sec = 0;
+  timeout.tv_nsec = 1;
+  set = block_sigchld();
   status = pam_authenticate (pamh, 0);
+# ifdef HAVE_SIGTIMEDWAIT
+  sigtimedwait (&set, NULL, &timeout);
+  /* #### What is the portable thing to do if we don't have it? */
+# endif /* HAVE_SIGTIMEDWAIT */
+  unblock_sigchld();
+
   if (verbose_p)
     fprintf (stderr, "%s:   pam_authenticate (...) ==> %d (%s)\n",
-             blurb(), status, pam_strerror(pamh, status));
+             blurb(), status, PAM_STRERROR(pamh, status));
+
+  if (status == PAM_SUCCESS)  /* Win! */
+    {
+      int status2;
+
+      /* We don't actually care if the account modules fail or succeed,
+       * but we need to run them anyway because certain pam modules
+       * depend on side effects of the account modules getting run.
+       */
+      status2 = pam_acct_mgmt (pamh, 0);
+
+      if (verbose_p)
+        fprintf (stderr, "%s:   pam_acct_mgmt (...) ==> %d (%s)\n",
+                 blurb(), status2, PAM_STRERROR(pamh, status2));
+
+      /* HPUX for some reason likes to make PAM defines different from
+       * everyone else's. */
+#ifdef PAM_AUTHTOKEN_REQD
+      if (status2 == PAM_AUTHTOKEN_REQD)
+#else
+      if (status2 == PAM_NEW_AUTHTOK_REQD)
+#endif
+        {
+          status2 = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
+          if (verbose_p)
+            fprintf (stderr, "%s: pam_chauthtok (...) ==> %d (%s)\n",
+                     blurb(), status2, PAM_STRERROR(pamh, status2));
+        }
+
+      /* Each time we successfully authenticate, refresh credentials,
+         for Kerberos/AFS/DCE/etc.  If this fails, just ignore that
+         failure and blunder along; it shouldn't matter.
+
+         Note: this used to be PAM_REFRESH_CRED instead of
+         PAM_REINITIALIZE_CRED, but Jason Heiss <jheiss@ee.washington.edu>
+         says that the Linux PAM library ignores that one, and only refreshes
+         credentials when using PAM_REINITIALIZE_CRED.
+       */
+      status2 = pam_setcred (pamh, PAM_REINITIALIZE_CRED);
+      if (verbose_p)
+        fprintf (stderr, "%s:   pam_setcred (...) ==> %d (%s)\n",
+                 blurb(), status2, PAM_STRERROR(pamh, status2));
+    }
 
  DONE:
-  if (user) free (user);
   if (pamh)
     {
       int status2 = pam_end (pamh, status);
@@ -192,106 +307,179 @@ pam_passwd_valid_p (const char *typed_passwd, Bool verbose_p)
                  blurb(), status2,
                  (status2 == PAM_SUCCESS ? "Success" : "Failure"));
     }
-  return (status == PAM_SUCCESS ? True : False);
+
+  if (status == PAM_SUCCESS)
+    si->unlock_state = ul_success;          /* yay */
+  else if (si->unlock_state == ul_cancel ||
+           si->unlock_state == ul_time)
+    ;                                       /* more specific failures ok */
+  else
+    si->unlock_state = ul_fail;                     /* generic failure */
 }
 
 
 Bool 
-pam_lock_init (int argc, char **argv, Bool verbose_p)
+pam_priv_init (int argc, char **argv, Bool verbose_p)
 {
   /* We have nothing to do at init-time.
      However, we might as well do some error checking.
      If "/etc/pam.d" exists and is a directory, but "/etc/pam.d/xlock"
      does not exist, warn that PAM probably isn't going to work.
+
+     This is a priv-init instead of a non-priv init in case the directory
+     is unreadable or something (don't know if that actually happens.)
    */
-  const char  dir[] = "/etc/pam.d";
-  const char file[] = "/etc/pam.d/" PAM_SERVICE_NAME;
+  const char   dir[] = "/etc/pam.d";
+  const char  file[] = "/etc/pam.d/" PAM_SERVICE_NAME;
+  const char file2[] = "/etc/pam.conf";
   struct stat st;
-  if (stat (dir, &st) == 0 && st.st_mode & S_IFDIR)
-    if (stat (file, &st) != 0)
+
+# ifndef S_ISDIR
+#  define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
+# endif
+
+  if (stat (dir, &st) == 0 && S_ISDIR(st.st_mode))
+    {
+      if (stat (file, &st) != 0)
+        fprintf (stderr,
+                 "%s: warning: %s does not exist.\n"
+                 "%s: password authentication via PAM is unlikely to work.\n",
+                 blurb(), file, blurb());
+    }
+  else if (stat (file2, &st) == 0)
+    {
+      FILE *f = fopen (file2, "r");
+      if (f)
+        {
+          Bool ok = False;
+          char buf[255];
+          while (fgets (buf, sizeof(buf), f))
+            if (strstr (buf, PAM_SERVICE_NAME))
+              {
+                ok = True;
+                break;
+              }
+          fclose (f);
+          if (!ok)
+            {
+              fprintf (stderr,
+                  "%s: warning: %s does not list the `%s' service.\n"
+                  "%s: password authentication via PAM is unlikely to work.\n",
+                       blurb(), file2, PAM_SERVICE_NAME, blurb());
+            }
+        }
+      /* else warn about file2 existing but being unreadable? */
+    }
+  else
+    {
       fprintf (stderr,
-               "%s: warning: %s does not exist.\n"
+               "%s: warning: neither %s nor %s exist.\n"
                "%s: password authentication via PAM is unlikely to work.\n",
-               blurb(), file, blurb());
+               blurb(), file2, file, blurb());
+    }
 
   /* Return true anyway, just in case. */
   return True;
 }
 
 
-/* This is the function PAM calls to have a conversation with the user.
-   Really, this function should be the thing that pops up dialog boxes
-   as needed, and prompts for various strings.
-
-   But, for now, xscreensaver uses its normal password-prompting dialog
-   first, and then this function simply returns the result that has been
-   typed.
-
-   This means that if PAM was using a retina scanner for auth, xscreensaver
-   would prompt for a password; then pam_conversation() would be called
-   with a string like "Please look into the retina scanner".  The user
-   would never see this string, and the prompted-for password would be
-   ignored.
- */
 static int
 pam_conversation (int nmsgs,
-                  const struct pam_message **msg,
-                  struct pam_response **resp,
-                  void *closure)
+                 const struct pam_message **msg,
+                 struct pam_response **resp,
+                 void *vsaver_info)
 {
-  int replies = 0;
-  struct pam_response *reply = 0;
-  struct pam_closure *c = (struct pam_closure *) closure;
-
-  reply = (struct pam_response *) calloc (nmsgs, sizeof (*reply));
-  if (!reply) return PAM_CONV_ERR;
-       
-  for (replies = 0; replies < nmsgs; replies++)
+  int i, ret = -1;
+  struct auth_message *messages = 0;
+  struct auth_response *authresp = 0;
+  struct pam_response *pam_responses;
+  saver_info *si = (saver_info *) vsaver_info;
+  Bool verbose_p;
+
+  /* On SunOS 5.6, the `closure' argument always comes in as random garbage. */
+  si = (saver_info *) suns_pam_implementation_blows;
+
+  verbose_p = si->prefs.verbose_p;
+
+  /* Converting the PAM prompts into the XScreenSaver native format.
+   * It was a design goal to collapse (INFO,PROMPT) pairs from PAM
+   * into a single call to the unlock_cb function. The unlock_cb function
+   * does that, but only if it is passed several prompts at a time. Most PAM
+   * modules only send a single prompt at a time, but because there is no way
+   * of telling whether there will be more prompts to follow, we can only ever
+   * pass along whatever was passed in here.
+   */
+
+  messages = calloc(nmsgs, sizeof(struct auth_message));
+  pam_responses = calloc(nmsgs, sizeof(*pam_responses));
+  
+  if (!pam_responses || !messages)
+    goto end;
+
+  if (verbose_p)
+    fprintf (stderr, "%s:     pam_conversation (", blurb());
+
+  for (i = 0; i < nmsgs; ++i)
     {
-      switch (msg[replies]->msg_style)
-        {
-        case PAM_PROMPT_ECHO_ON:
-          reply[replies].resp_retcode = PAM_SUCCESS;
-          reply[replies].resp = strdup (c->user);         /* freed by PAM */
-          if (c->verbose_p)
-            fprintf (stderr, "%s:     PAM ECHO_ON(\"%s\") ==> \"%s\"\n",
-                     blurb(), msg[replies]->msg,
-                     reply[replies].resp);
-          break;
-        case PAM_PROMPT_ECHO_OFF:
-          reply[replies].resp_retcode = PAM_SUCCESS;
-          reply[replies].resp = strdup (c->typed_passwd);   /* freed by PAM */
-          if (c->verbose_p)
-            fprintf (stderr, "%s:     PAM ECHO_OFF(\"%s\") ==> password\n",
-                     blurb(), msg[replies]->msg);
-          break;
-        case PAM_TEXT_INFO:
-          /* ignore it... */
-          reply[replies].resp_retcode = PAM_SUCCESS;
-          reply[replies].resp = 0;
-          if (c->verbose_p)
-            fprintf (stderr, "%s:     PAM TEXT_INFO(\"%s\") ==> ignored\n",
-                     blurb(), msg[replies]->msg);
-          break;
-        case PAM_ERROR_MSG:
-          /* ignore it... */
-          reply[replies].resp_retcode = PAM_SUCCESS;
-          reply[replies].resp = 0;
-          if (c->verbose_p)
-            fprintf (stderr, "%s:     PAM ERROR_MSG(\"%s\") ==> ignored\n",
-                     blurb(), msg[replies]->msg);
-          break;
-        default:
-          /* Must be an error of some sort... */
-          free (reply);
-          if (c->verbose_p)
-            fprintf (stderr, "%s:     PAM unknown %d(\"%s\") ==> ignored\n",
-                     blurb(), msg[replies]->msg_style, msg[replies]->msg);
-          return PAM_CONV_ERR;
-        }
+      if (verbose_p && i > 0) fprintf (stderr, ", ");
+
+      messages[i].msg = msg[i]->msg;
+
+      switch (msg[i]->msg_style) {
+      case PAM_PROMPT_ECHO_OFF: messages[i].type = AUTH_MSGTYPE_PROMPT_NOECHO;
+        if (verbose_p) fprintf (stderr, "ECHO_OFF");
+        break;
+      case PAM_PROMPT_ECHO_ON:  messages[i].type = AUTH_MSGTYPE_PROMPT_ECHO;
+        if (verbose_p) fprintf (stderr, "ECHO_ON");
+        break;
+      case PAM_ERROR_MSG:       messages[i].type = AUTH_MSGTYPE_ERROR;
+        if (verbose_p) fprintf (stderr, "ERROR_MSG");
+        break;
+      case PAM_TEXT_INFO:       messages[i].type = AUTH_MSGTYPE_INFO;
+        if (verbose_p) fprintf (stderr, "TEXT_INFO");
+        break;
+      default:                  messages[i].type = AUTH_MSGTYPE_PROMPT_ECHO;
+        if (verbose_p) fprintf (stderr, "PROMPT_ECHO");
+        break;
+      }
+
+      if (verbose_p) 
+        fprintf (stderr, "=\"%s\"", msg[i]->msg ? msg[i]->msg : "(null)");
+    }
+
+  if (verbose_p)
+    fprintf (stderr, ") ...\n");
+
+  ret = si->unlock_cb(nmsgs, messages, &authresp, si);
+
+  if (ret == 0)
+    {
+      for (i = 0; i < nmsgs; ++i)
+       pam_responses[i].resp = authresp[i].response;
     }
-  *resp = reply;
-  return PAM_SUCCESS;
+
+end:
+  if (messages)
+    free(messages);
+
+  if (authresp)
+    free(authresp);
+
+  if (verbose_p)
+    fprintf (stderr, "%s:     pam_conversation (...) ==> %s\n", blurb(),
+             (ret == 0 ? "PAM_SUCCESS" : "PAM_CONV_ERR"));
+
+  if (ret == 0)
+    {
+      *resp = pam_responses;
+      return PAM_SUCCESS;
+    }
+
+  /* Failure only */
+    if (pam_responses)
+      free(pam_responses);
+
+    return PAM_CONV_ERR;
 }
 
 #endif /* NO_LOCKING -- whole file */