From http://www.jwz.org/xscreensaver/xscreensaver-5.27.tar.gz
[xscreensaver] / driver / passwd-pam.c
1 /* passwd-pam.c --- verifying typed passwords with PAM
2  * (Pluggable Authentication Modules.)
3  * written by Bill Nottingham <notting@redhat.com> (and jwz) for
4  * xscreensaver, Copyright (c) 1993-2012 Jamie Zawinski <jwz@jwz.org>
5  *
6  * Permission to use, copy, modify, distribute, and sell this software and its
7  * documentation for any purpose is hereby granted without fee, provided that
8  * the above copyright notice appear in all copies and that both that
9  * copyright notice and this permission notice appear in supporting
10  * documentation.  No representations are made about the suitability of this
11  * software for any purpose.  It is provided "as is" without express or 
12  * implied warranty.
13  *
14  * Some PAM resources:
15  *
16  *    PAM home page:
17  *    http://www.us.kernel.org/pub/linux/libs/pam/
18  *
19  *    PAM FAQ:
20  *    http://www.us.kernel.org/pub/linux/libs/pam/FAQ
21  *
22  *    PAM Application Developers' Guide:
23  *    http://www.us.kernel.org/pub/linux/libs/pam/Linux-PAM-html/Linux-PAM_ADG.html
24  *
25  *    PAM Mailing list archives:
26  *    http://www.linuxhq.com/lnxlists/linux-pam/
27  *
28  *    Compatibility notes, especially between Linux and Solaris:
29  *    http://www.contrib.andrew.cmu.edu/u/shadow/pam.html
30  *
31  *    The Open Group's PAM API documentation:
32  *    http://www.opengroup.org/onlinepubs/8329799/pam_start.htm
33  */
34
35 #ifdef HAVE_CONFIG_H
36 # include "config.h"
37 #endif
38
39 #ifndef NO_LOCKING  /* whole file */
40
41 #include <stdlib.h>
42 #ifdef HAVE_UNISTD_H
43 # include <unistd.h>
44 #endif
45
46 extern char *blurb(void);
47
48
49 #include <stdio.h>
50 #include <string.h>
51 #include <sys/types.h>
52 #include <pwd.h>
53 #include <grp.h>
54 #include <security/pam_appl.h>
55 #include <signal.h>
56 #include <errno.h>
57 #include <X11/Intrinsic.h>
58
59 #include <sys/stat.h>
60
61 #include "auth.h"
62
63 extern sigset_t block_sigchld (void);
64 extern void unblock_sigchld (void);
65
66 /* blargh */
67 #undef  Bool
68 #undef  True
69 #undef  False
70 #define Bool  int
71 #define True  1
72 #define False 0
73
74 #undef countof
75 #define countof(x) (sizeof((x))/sizeof(*(x)))
76
77 /* Some time between Red Hat 4.2 and 7.0, the words were transposed 
78    in the various PAM_x_CRED macro names.  Yay!
79  */
80 #ifndef  PAM_REFRESH_CRED
81 # define PAM_REFRESH_CRED PAM_CRED_REFRESH
82 #endif
83
84 static int pam_conversation (int nmsgs,
85                              const struct pam_message **msg,
86                              struct pam_response **resp,
87                              void *closure);
88
89 void pam_try_unlock(saver_info *si, Bool verbose_p,
90                Bool (*valid_p)(const char *typed_passwd, Bool verbose_p));
91
92 Bool pam_priv_init (int argc, char **argv, Bool verbose_p);
93
94 #ifdef HAVE_PAM_FAIL_DELAY
95    /* We handle delays ourself.*/
96    /* Don't set this to 0 (Linux bug workaround.) */
97 # define PAM_NO_DELAY(pamh) pam_fail_delay ((pamh), 1)
98 #else  /* !HAVE_PAM_FAIL_DELAY */
99 # define PAM_NO_DELAY(pamh) /* */
100 #endif /* !HAVE_PAM_FAIL_DELAY */
101
102
103 /* On SunOS 5.6, and on Linux with PAM 0.64, pam_strerror() takes two args.
104    On some other Linux systems with some other version of PAM (e.g.,
105    whichever Debian release comes with a 2.2.5 kernel) it takes one arg.
106    I can't tell which is more "recent" or "correct" behavior, so configure
107    figures out which is in use for us.  Shoot me!
108  */
109 #ifdef PAM_STRERROR_TWO_ARGS
110 # define PAM_STRERROR(pamh, status) pam_strerror((pamh), (status))
111 #else  /* !PAM_STRERROR_TWO_ARGS */
112 # define PAM_STRERROR(pamh, status) pam_strerror((status))
113 #endif /* !PAM_STRERROR_TWO_ARGS */
114
115
116 /* PAM sucks in that there is no way to tell whether a particular service
117    is configured at all.  That is, there is no way to tell the difference
118    between "authentication of the FOO service is not allowed" and "the
119    user typed the wrong password."
120
121    On RedHat 5.1 systems, if a service name is not known, it defaults to
122    being not allowed (because the fallback service, /etc/pam.d/other, is
123    set to `pam_deny'.)
124
125    On Solaris 2.6 systems, unknown services default to authenticating normally.
126
127    So, we could simply require that the person who installs xscreensaver
128    set up an "xscreensaver" PAM service.  However, if we went that route,
129    it would have a really awful failure mode: the failure mode would be that
130    xscreensaver was willing to *lock* the screen, but would be unwilling to
131    *unlock* the screen.  (With the non-PAM password code, the analagous
132    situation -- security not being configured properly, for example do to the
133    executable not being installed as setuid root -- the failure mode is much
134    more palettable, in that xscreensaver will refuse to *lock* the screen,
135    because it can know up front that there is no password that will work.)
136
137    Another route would be to have the service name to consult be computed at
138    compile-time (perhaps with a configure option.)  However, that doesn't
139    really solve the problem, because it means that the same executable might
140    work fine on one machine, but refuse to unlock when run on another
141    machine.
142
143    Another alternative would be to look in /etc/pam.conf or /etc/pam.d/ at
144    runtime to see what services actually exist.  But I think that's no good,
145    because who is to say that the PAM info is actually specified in those
146    files?  Opening and reading those files is not a part of the PAM client
147    API, so it's not guarenteed to work on any given system.
148
149    An alternative I tried was to specify a list of services to try, and to
150    try them all in turn ("xscreensaver", "xlock", "xdm", and "login").
151    This worked, but it was slow (and I also had to do some contortions to
152    work around bugs in Linux PAM 0.64-3.)
153
154    So what we do today is, try PAM once, and if that fails, try the usual
155    getpwent() method.  So if PAM doesn't work, it will at least make an
156    attempt at looking up passwords in /etc/passwd or /etc/shadow instead.
157
158    This all kind of blows.  I'm not sure what else to do.
159  */
160
161
162 /* On SunOS 5.6, the `pam_conv.appdata_ptr' slot seems to be ignored, and
163    the `closure' argument to pc.conv always comes in as random garbage.
164    So we get around this by using a global variable instead.  Shoot me!
165
166    (I've been told this is bug 4092227, and is fixed in Solaris 7.)
167    (I've also been told that it's fixed in Solaris 2.6 by patch 106257-05.)
168  */
169 static void *suns_pam_implementation_blows = 0;
170
171
172 /**
173  * This function is the PAM conversation driver. It conducts a full
174  * authentication round by invoking the GUI with various prompts.
175  */
176 void
177 pam_try_unlock(saver_info *si, Bool verbose_p,
178                Bool (*valid_p)(const char *typed_passwd, Bool verbose_p))
179 {
180   const char *service = PAM_SERVICE_NAME;
181   pam_handle_t *pamh = 0;
182   int status = -1;
183   struct pam_conv pc;
184   sigset_t set;
185   struct timespec timeout;
186
187   pc.conv = &pam_conversation;
188   pc.appdata_ptr = (void *) si;
189
190   /* On SunOS 5.6, the `appdata_ptr' slot seems to be ignored, and the
191      `closure' argument to pc.conv always comes in as random garbage. */
192   suns_pam_implementation_blows = (void *) si;
193
194
195   /* Initialize PAM.
196    */
197   status = pam_start (service, si->user, &pc, &pamh);
198   if (verbose_p)
199     fprintf (stderr, "%s: pam_start (\"%s\", \"%s\", ...) ==> %d (%s)\n",
200              blurb(), service, si->user,
201              status, PAM_STRERROR (pamh, status));
202   if (status != PAM_SUCCESS) goto DONE;
203
204   /* #### We should set PAM_TTY to the display we're using, but we
205      don't have that handy from here.  So set it to :0.0, which is a
206      good guess (and has the bonus of counting as a "secure tty" as
207      far as PAM is concerned...)
208    */
209   {
210     char *tty = strdup (":0.0");
211     status = pam_set_item (pamh, PAM_TTY, tty);
212     if (verbose_p)
213       fprintf (stderr, "%s:   pam_set_item (p, PAM_TTY, \"%s\") ==> %d (%s)\n",
214                blurb(), tty, status, PAM_STRERROR(pamh, status));
215     free (tty);
216   }
217
218   /* Try to authenticate as the current user.
219      We must turn off our SIGCHLD handler for the duration of the call to
220      pam_authenticate(), because in some cases, the underlying PAM code
221      will do this:
222
223         1: fork a setuid subprocess to do some dirty work;
224         2: read a response from that subprocess;
225         3: waitpid(pid, ...) on that subprocess.
226
227     If we (the ignorant parent process) have a SIGCHLD handler, then there's
228     a race condition between steps 2 and 3: if the subprocess exits before
229     waitpid() was called, then our SIGCHLD handler fires, and gets notified
230     of the subprocess death; then PAM's call to waitpid() fails, because the
231     process has already been reaped.
232
233     I consider this a bug in PAM, since the caller should be able to have
234     whatever signal handlers it wants -- the PAM documentation doesn't say
235     "oh by the way, if you use PAM, you can't use SIGCHLD."
236    */
237
238   PAM_NO_DELAY(pamh);
239
240   if (verbose_p)
241     fprintf (stderr, "%s:   pam_authenticate (...) ...\n", blurb());
242
243   timeout.tv_sec = 0;
244   timeout.tv_nsec = 1;
245   set = block_sigchld();
246   status = pam_authenticate (pamh, 0);
247 # ifdef HAVE_SIGTIMEDWAIT
248   sigtimedwait (&set, NULL, &timeout);
249   /* #### What is the portable thing to do if we don't have it? */
250 # endif /* HAVE_SIGTIMEDWAIT */
251   unblock_sigchld();
252
253   if (verbose_p)
254     fprintf (stderr, "%s:   pam_authenticate (...) ==> %d (%s)\n",
255              blurb(), status, PAM_STRERROR(pamh, status));
256
257   if (status == PAM_SUCCESS)  /* Win! */
258     {
259       int status2;
260
261       /* On most systems, it doesn't matter whether the account modules
262          are run, or whether they fail or succeed.
263
264          On some systems, the account modules fail, because they were
265          never configured properly, but it's necessary to run them anyway
266          because certain PAM modules depend on side effects of the account
267          modules having been run.
268
269          And on still other systems, the account modules are actually
270          used, and failures in them should be considered to be true!
271
272          So:
273          - We run the account modules on all systems.
274          - Whether we ignore them is a configure option.
275
276          It's all kind of a mess.
277        */
278       status2 = pam_acct_mgmt (pamh, 0);
279
280       if (verbose_p)
281         fprintf (stderr, "%s:   pam_acct_mgmt (...) ==> %d (%s)\n",
282                  blurb(), status2, PAM_STRERROR(pamh, status2));
283
284       /* HPUX for some reason likes to make PAM defines different from
285        * everyone else's. */
286 #ifdef PAM_AUTHTOKEN_REQD
287       if (status2 == PAM_AUTHTOKEN_REQD)
288 #else
289       if (status2 == PAM_NEW_AUTHTOK_REQD)
290 #endif
291         {
292           status2 = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
293           if (verbose_p)
294             fprintf (stderr, "%s: pam_chauthtok (...) ==> %d (%s)\n",
295                      blurb(), status2, PAM_STRERROR(pamh, status2));
296         }
297
298       /* If 'configure' requested that we believe the results of PAM
299          account module failures, then obey that status code.
300          Otherwise ignore it.
301        */
302 #ifdef PAM_CHECK_ACCOUNT_TYPE
303        status = status2;
304 #endif
305
306       /* Each time we successfully authenticate, refresh credentials,
307          for Kerberos/AFS/DCE/etc.  If this fails, just ignore that
308          failure and blunder along; it shouldn't matter.
309
310          Note: this used to be PAM_REFRESH_CRED instead of
311          PAM_REINITIALIZE_CRED, but Jason Heiss <jheiss@ee.washington.edu>
312          says that the Linux PAM library ignores that one, and only refreshes
313          credentials when using PAM_REINITIALIZE_CRED.
314        */
315       status2 = pam_setcred (pamh, PAM_REINITIALIZE_CRED);
316       if (verbose_p)
317         fprintf (stderr, "%s:   pam_setcred (...) ==> %d (%s)\n",
318                  blurb(), status2, PAM_STRERROR(pamh, status2));
319     }
320
321  DONE:
322   if (pamh)
323     {
324       int status2 = pam_end (pamh, status);
325       pamh = 0;
326       if (verbose_p)
327         fprintf (stderr, "%s: pam_end (...) ==> %d (%s)\n",
328                  blurb(), status2,
329                  (status2 == PAM_SUCCESS ? "Success" : "Failure"));
330     }
331
332   if (status == PAM_SUCCESS)
333     si->unlock_state = ul_success;           /* yay */
334   else if (si->unlock_state == ul_cancel ||
335            si->unlock_state == ul_time)
336     ;                                        /* more specific failures ok */
337   else
338     si->unlock_state = ul_fail;              /* generic failure */
339 }
340
341
342 Bool 
343 pam_priv_init (int argc, char **argv, Bool verbose_p)
344 {
345   /* We have nothing to do at init-time.
346      However, we might as well do some error checking.
347      If "/etc/pam.d" exists and is a directory, but "/etc/pam.d/xlock"
348      does not exist, warn that PAM probably isn't going to work.
349
350      This is a priv-init instead of a non-priv init in case the directory
351      is unreadable or something (don't know if that actually happens.)
352    */
353   const char   dir[] = "/etc/pam.d";
354   const char  file[] = "/etc/pam.d/" PAM_SERVICE_NAME;
355   const char file2[] = "/etc/pam.conf";
356   struct stat st;
357
358 # ifndef S_ISDIR
359 #  define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
360 # endif
361
362   if (stat (dir, &st) == 0 && S_ISDIR(st.st_mode))
363     {
364       if (stat (file, &st) != 0)
365         fprintf (stderr,
366                  "%s: warning: %s does not exist.\n"
367                  "%s: password authentication via PAM is unlikely to work.\n",
368                  blurb(), file, blurb());
369     }
370   else if (stat (file2, &st) == 0)
371     {
372       FILE *f = fopen (file2, "r");
373       if (f)
374         {
375           Bool ok = False;
376           char buf[255];
377           while (fgets (buf, sizeof(buf), f))
378             if (strstr (buf, PAM_SERVICE_NAME))
379               {
380                 ok = True;
381                 break;
382               }
383           fclose (f);
384           if (!ok)
385             {
386               fprintf (stderr,
387                   "%s: warning: %s does not list the `%s' service.\n"
388                   "%s: password authentication via PAM is unlikely to work.\n",
389                        blurb(), file2, PAM_SERVICE_NAME, blurb());
390             }
391         }
392       /* else warn about file2 existing but being unreadable? */
393     }
394   else
395     {
396       fprintf (stderr,
397                "%s: warning: neither %s nor %s exist.\n"
398                "%s: password authentication via PAM is unlikely to work.\n",
399                blurb(), file2, file, blurb());
400     }
401
402   /* Return true anyway, just in case. */
403   return True;
404 }
405
406
407 static int
408 pam_conversation (int nmsgs,
409                   const struct pam_message **msg,
410                   struct pam_response **resp,
411                   void *vsaver_info)
412 {
413   int i, ret = -1;
414   struct auth_message *messages = 0;
415   struct auth_response *authresp = 0;
416   struct pam_response *pam_responses;
417   saver_info *si = (saver_info *) vsaver_info;
418   Bool verbose_p;
419
420   /* On SunOS 5.6, the `closure' argument always comes in as random garbage. */
421   si = (saver_info *) suns_pam_implementation_blows;
422
423   verbose_p = si->prefs.verbose_p;
424
425   /* Converting the PAM prompts into the XScreenSaver native format.
426    * It was a design goal to collapse (INFO,PROMPT) pairs from PAM
427    * into a single call to the unlock_cb function. The unlock_cb function
428    * does that, but only if it is passed several prompts at a time. Most PAM
429    * modules only send a single prompt at a time, but because there is no way
430    * of telling whether there will be more prompts to follow, we can only ever
431    * pass along whatever was passed in here.
432    */
433
434   messages = calloc(nmsgs, sizeof(struct auth_message));
435   pam_responses = calloc(nmsgs, sizeof(*pam_responses));
436   
437   if (!pam_responses || !messages)
438     goto end;
439
440   if (verbose_p)
441     fprintf (stderr, "%s:     pam_conversation (", blurb());
442
443   for (i = 0; i < nmsgs; ++i)
444     {
445       if (verbose_p && i > 0) fprintf (stderr, ", ");
446
447       messages[i].msg = msg[i]->msg;
448
449       switch (msg[i]->msg_style) {
450       case PAM_PROMPT_ECHO_OFF: messages[i].type = AUTH_MSGTYPE_PROMPT_NOECHO;
451         if (verbose_p) fprintf (stderr, "ECHO_OFF");
452         break;
453       case PAM_PROMPT_ECHO_ON:  messages[i].type = AUTH_MSGTYPE_PROMPT_ECHO;
454         if (verbose_p) fprintf (stderr, "ECHO_ON");
455         break;
456       case PAM_ERROR_MSG:       messages[i].type = AUTH_MSGTYPE_ERROR;
457         if (verbose_p) fprintf (stderr, "ERROR_MSG");
458         break;
459       case PAM_TEXT_INFO:       messages[i].type = AUTH_MSGTYPE_INFO;
460         if (verbose_p) fprintf (stderr, "TEXT_INFO");
461         break;
462       default:                  messages[i].type = AUTH_MSGTYPE_PROMPT_ECHO;
463         if (verbose_p) fprintf (stderr, "PROMPT_ECHO");
464         break;
465       }
466
467       if (verbose_p) 
468         fprintf (stderr, "=\"%s\"", msg[i]->msg ? msg[i]->msg : "(null)");
469     }
470
471   if (verbose_p)
472     fprintf (stderr, ") ...\n");
473
474   ret = si->unlock_cb(nmsgs, messages, &authresp, si);
475
476   /* #### If the user times out, or hits ESC or Cancel, we return PAM_CONV_ERR,
477           and PAM logs this as an authentication failure.  It would be nice if
478           there was some way to indicate that this was a "cancel" rather than
479           a "fail", so that it wouldn't show up in syslog, but I think the
480           only options are PAM_SUCCESS and PAM_CONV_ERR.  (I think that
481           PAM_ABORT means "internal error", not "cancel".)  Bleh.
482    */
483
484   if (ret == 0)
485     {
486       for (i = 0; i < nmsgs; ++i)
487         pam_responses[i].resp = authresp[i].response;
488     }
489
490 end:
491   if (messages)
492     free(messages);
493
494   if (authresp)
495     free(authresp);
496
497   if (verbose_p)
498     fprintf (stderr, "%s:     pam_conversation (...) ==> %s\n", blurb(),
499              (ret == 0 ? "PAM_SUCCESS" : "PAM_CONV_ERR"));
500
501   if (ret == 0)
502     {
503       *resp = pam_responses;
504       return PAM_SUCCESS;
505     }
506
507   /* Failure only */
508     if (pam_responses)
509       free(pam_responses);
510
511     return PAM_CONV_ERR;
512 }
513
514 #endif /* NO_LOCKING -- whole file */