448cd9cbf8da80d7a06fcdaf1fe3005d8b18e83b
[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-2003 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   timeout.tv_sec = 0;
241   timeout.tv_nsec = 1;
242   set = block_sigchld();
243   status = pam_authenticate (pamh, 0);
244   sigtimedwait (&set, NULL, &timeout);
245   unblock_sigchld();
246
247   if (verbose_p)
248     fprintf (stderr, "%s:   pam_authenticate (...) ==> %d (%s)\n",
249              blurb(), status, PAM_STRERROR(pamh, status));
250
251   if (status == PAM_SUCCESS)  /* Win! */
252     {
253       int status2;
254
255       /* We don't actually care if the account modules fail or succeed,
256        * but we need to run them anyway because certain pam modules
257        * depend on side effects of the account modules getting run.
258        */
259       status2 = pam_acct_mgmt (pamh, 0);
260
261       if (verbose_p)
262         fprintf (stderr, "%s:   pam_acct_mgmt (...) ==> %d (%s)\n",
263                  blurb(), status2, PAM_STRERROR(pamh, status2));
264
265       /* HPUX for some reason likes to make PAM defines different from
266        * everyone else's. */
267 #ifdef PAM_AUTHTOKEN_REQD
268       if (status2 == PAM_AUTHTOKEN_REQD)
269 #else
270       if (status2 == PAM_NEW_AUTHTOK_REQD)
271 #endif
272         {
273           status2 = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
274           if (verbose_p)
275             fprintf (stderr, "%s: pam_chauthtok (...) ==> %d (%s)\n",
276                      blurb(), status2, PAM_STRERROR(pamh, status2));
277         }
278
279       /* Each time we successfully authenticate, refresh credentials,
280          for Kerberos/AFS/DCE/etc.  If this fails, just ignore that
281          failure and blunder along; it shouldn't matter.
282
283          Note: this used to be PAM_REFRESH_CRED instead of
284          PAM_REINITIALIZE_CRED, but Jason Heiss <jheiss@ee.washington.edu>
285          says that the Linux PAM library ignores that one, and only refreshes
286          credentials when using PAM_REINITIALIZE_CRED.
287        */
288       status2 = pam_setcred (pamh, PAM_REINITIALIZE_CRED);
289       if (verbose_p)
290         fprintf (stderr, "%s:   pam_setcred (...) ==> %d (%s)\n",
291                  blurb(), status2, PAM_STRERROR(pamh, status2));
292     }
293
294  DONE:
295   if (pamh)
296     {
297       int status2 = pam_end (pamh, status);
298       pamh = 0;
299       if (verbose_p)
300         fprintf (stderr, "%s: pam_end (...) ==> %d (%s)\n",
301                  blurb(), status2,
302                  (status2 == PAM_SUCCESS ? "Success" : "Failure"));
303     }
304
305   si->unlock_state = (status == PAM_SUCCESS) ? ul_success : ul_fail;
306 }
307
308
309 Bool 
310 pam_priv_init (int argc, char **argv, Bool verbose_p)
311 {
312   /* We have nothing to do at init-time.
313      However, we might as well do some error checking.
314      If "/etc/pam.d" exists and is a directory, but "/etc/pam.d/xlock"
315      does not exist, warn that PAM probably isn't going to work.
316
317      This is a priv-init instead of a non-priv init in case the directory
318      is unreadable or something (don't know if that actually happens.)
319    */
320   const char   dir[] = "/etc/pam.d";
321   const char  file[] = "/etc/pam.d/" PAM_SERVICE_NAME;
322   const char file2[] = "/etc/pam.conf";
323   struct stat st;
324
325 # ifndef S_ISDIR
326 #  define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
327 # endif
328
329   if (stat (dir, &st) == 0 && S_ISDIR(st.st_mode))
330     {
331       if (stat (file, &st) != 0)
332         fprintf (stderr,
333                  "%s: warning: %s does not exist.\n"
334                  "%s: password authentication via PAM is unlikely to work.\n",
335                  blurb(), file, blurb());
336     }
337   else if (stat (file2, &st) == 0)
338     {
339       FILE *f = fopen (file2, "r");
340       if (f)
341         {
342           Bool ok = False;
343           char buf[255];
344           while (fgets (buf, sizeof(buf), f))
345             if (strstr (buf, PAM_SERVICE_NAME))
346               {
347                 ok = True;
348                 break;
349               }
350           fclose (f);
351           if (!ok)
352             {
353               fprintf (stderr,
354                   "%s: warning: %s does not list the `%s' service.\n"
355                   "%s: password authentication via PAM is unlikely to work.\n",
356                        blurb(), file2, PAM_SERVICE_NAME, blurb());
357             }
358         }
359       /* else warn about file2 existing but being unreadable? */
360     }
361   else
362     {
363       fprintf (stderr,
364                "%s: warning: neither %s nor %s exist.\n"
365                "%s: password authentication via PAM is unlikely to work.\n",
366                blurb(), file2, file, blurb());
367     }
368
369   /* Return true anyway, just in case. */
370   return True;
371 }
372
373
374 static int
375 pam_conversation (int nmsgs,
376                   const struct pam_message **msg,
377                   struct pam_response **resp,
378                   void *vsaver_info)
379 {
380   int i, ret = -1;
381   struct auth_message *messages = 0;
382   struct auth_response *authresp = 0;
383   struct pam_response *pam_responses;
384   saver_info *si = (saver_info *) vsaver_info;
385
386   /* On SunOS 5.6, the `closure' argument always comes in as random garbage. */
387   si = (saver_info *) suns_pam_implementation_blows;
388
389   /* Converting the PAM prompts into the XScreenSaver native format.
390    * It was a design goal to collapse (INFO,PROMPT) pairs from PAM
391    * into a single call to the unlock_cb function. The unlock_cb function
392    * does that, but only if it is passed several prompts at a time. Most PAM
393    * modules only send a single prompt at a time, but because there is no way
394    * of telling whether there will be more prompts to follow, we can only ever
395    * pass along whatever was passed in here.
396    */
397
398   messages = calloc(nmsgs, sizeof(struct auth_message));
399   pam_responses = calloc(nmsgs, sizeof(*pam_responses));
400   
401   if (!pam_responses || !messages)
402     goto end;
403
404   for (i = 0; i < nmsgs; ++i)
405     {
406       messages[i].msg = msg[i]->msg;
407
408       /* Default fallback of PROMPT_ECHO */
409       messages[i].type = 
410         msg[i]->msg_style == PAM_PROMPT_ECHO_OFF
411         ? AUTH_MSGTYPE_PROMPT_NOECHO
412         : msg[i]->msg_style == PAM_PROMPT_ECHO_ON
413           ? AUTH_MSGTYPE_PROMPT_ECHO
414           : msg[i]->msg_style == PAM_ERROR_MSG
415             ? AUTH_MSGTYPE_ERROR
416             : msg[i]->msg_style == PAM_TEXT_INFO
417               ? AUTH_MSGTYPE_INFO
418               : AUTH_MSGTYPE_PROMPT_ECHO;
419     }
420
421   ret = si->unlock_cb(nmsgs, messages, &authresp, si);
422
423   if (ret == 0)
424     {
425       for (i = 0; i < nmsgs; ++i)
426         pam_responses[i].resp = authresp[i].response;
427     }
428
429 end:
430   if (messages)
431     free(messages);
432
433   if (authresp)
434     free(authresp);
435
436   if (ret == 0)
437     {
438       *resp = pam_responses;
439       return PAM_SUCCESS;
440     }
441
442   /* Failure only */
443     if (pam_responses)
444       free(pam_responses);
445
446     return PAM_CONV_ERR;
447 }
448
449 #endif /* NO_LOCKING -- whole file */