1 /*
2  * Copyright 2003,2004,2005,2006,2007,2008,2009,2010,2011,2014 Red Hat, Inc.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, and the entire permission notice in its entirety,
9  *    including the disclaimer of warranties.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  * 3. The name of the author may not be used to endorse or promote
14  *    products derived from this software without specific prior
15  *    written permission.
16  *
17  * ALTERNATIVELY, this product may be distributed under the terms of the
18  * GNU Lesser General Public License, in which case the provisions of the
19  * LGPL are required INSTEAD OF the above restrictions.
20  *
21  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
22  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
23  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
24  * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #include "../config.h"
34 
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 
38 #ifdef HAVE_SECURITY_PAM_APPL_H
39 #include <security/pam_appl.h>
40 #endif
41 
42 #ifdef HAVE_SECURITY_PAM_MODULES_H
43 #define PAM_SM_PASSWORD
44 #include <security/pam_modules.h>
45 #endif
46 
47 #include <limits.h>
48 #include <stdio.h>
49 #include <stdlib.h>
50 #include <string.h>
51 
52 #include KRB5_H
53 
54 #include "conv.h"
55 #include "init.h"
56 #include "initopts.h"
57 #include "items.h"
58 #include "log.h"
59 #include "options.h"
60 #include "prompter.h"
61 #include "stash.h"
62 #include "userinfo.h"
63 #include "v5.h"
64 #include "xstr.h"
65 
66 int
pam_sm_chauthtok(pam_handle_t * pamh,int flags,int argc,PAM_KRB5_MAYBE_CONST char ** argv)67 pam_sm_chauthtok(pam_handle_t *pamh, int flags,
68 		 int argc, PAM_KRB5_MAYBE_CONST char **argv)
69 {
70 	PAM_KRB5_MAYBE_CONST char *user;
71 	char prompt[LINE_MAX], prompt2[LINE_MAX], *password, *password2;
72 	krb5_context ctx;
73 	krb5_creds pwc_creds;
74 	struct _pam_krb5_options *options;
75 	struct _pam_krb5_user_info *userinfo;
76 	struct _pam_krb5_stash *stash;
77 	krb5_get_init_creds_opt *gic_options, *tmp_gicopts;
78 	int tmp_result, prelim_attempted;
79 	int i, retval, use_third_pass;
80 	char *pwhelp;
81 	struct stat st;
82 	FILE *fp;
83 	struct pam_message message;
84 
85 	/* Initialize Kerberos. */
86 	if (_pam_krb5_init_ctx(&ctx, argc, argv) != 0) {
87 		warn("error initializing Kerberos");
88 		return PAM_SERVICE_ERR;
89 	}
90 
91 	/* Get the user's name. */
92 	i = pam_get_user(pamh, &user, NULL);
93 	if ((i != PAM_SUCCESS) || (user == NULL)) {
94 		warn("could not identify user name");
95 		_pam_krb5_free_ctx(ctx);
96 		return i;
97 	}
98 
99 	/* Read our options. */
100 	i = v5_alloc_get_init_creds_opt(ctx, &gic_options);
101 	if (i != 0) {
102 		warn("error initializing options (shouldn't happen)");
103 		_pam_krb5_free_ctx(ctx);
104 		return PAM_SERVICE_ERR;
105 	}
106 	options = _pam_krb5_options_init(pamh, argc, argv, ctx,
107 					 _pam_krb5_option_role_chauthtok);
108 	if (options == NULL) {
109 		warn("error parsing options (shouldn't happen)");
110 		v5_free_get_init_creds_opt(ctx, gic_options);
111 		_pam_krb5_free_ctx(ctx);
112 		return PAM_SERVICE_ERR;
113 	}
114 	_pam_krb5_set_init_opts(ctx, gic_options, options);
115 
116 	/* Get information about the user and the user's principal name. */
117 	userinfo = _pam_krb5_user_info_init(ctx, user, options);
118 	if (userinfo == NULL) {
119 		if (options->ignore_unknown_principals) {
120 			retval = PAM_IGNORE;
121 		} else {
122 			warn("error getting information about '%s'", user);
123 			retval = PAM_USER_UNKNOWN;
124 		}
125 		_pam_krb5_options_free(pamh, ctx, options);
126 		v5_free_get_init_creds_opt(ctx, gic_options);
127 		_pam_krb5_free_ctx(ctx);
128 		return retval;
129 	}
130 
131 	/* Check the minimum UID argument. */
132 	if ((options->user_check) &&
133 	    (options->minimum_uid != (uid_t)-1) &&
134 	    (userinfo->uid < options->minimum_uid)) {
135 		if (options->debug) {
136 			debug("ignoring '%s' -- uid below minimum = %lu", user,
137 			      (unsigned long) options->minimum_uid);
138 		}
139 		_pam_krb5_user_info_free(ctx, userinfo);
140 		_pam_krb5_options_free(pamh, ctx, options);
141 		v5_free_get_init_creds_opt(ctx, gic_options);
142 		_pam_krb5_free_ctx(ctx);
143 		return PAM_IGNORE;
144 	}
145 
146 	/* Get the stash of credentials.  If we are interactively prompting
147 	 * the user for information, we're not expected to ask for the user's
148 	 * current password more than once, so we use it to get a changepw
149 	 * ticket during the first pass, and we store that for use in the
150 	 * second pass.  It should have a low lifetime, so we needn't free it
151 	 * just now. */
152 	retval = PAM_AUTH_ERR;
153 	stash = _pam_krb5_stash_get(pamh, user, userinfo, options);
154 
155 	/* If this is the first pass, just check the user's password by
156 	 * obtaining a password-changing initial ticket. */
157 	if (flags & PAM_PRELIM_CHECK) {
158 		retval = PAM_AUTH_ERR;
159 		prelim_attempted = 0;
160 
161 		/* Ideally we're only going to let libkrb5 ask questions once,
162 		 * and after that we intend to lie to it. */
163 		use_third_pass = options->use_third_pass;
164 
165 		/* Set up options for getting password-changing creds. */
166 		i = v5_alloc_get_init_creds_opt(ctx, &tmp_gicopts);
167 		if (i == 0) {
168 			/* Set hard-coded defaults for password-changing creds
169 			 * which might not match generally-used options. */
170 			_pam_krb5_set_init_opts_for_pwchange(ctx,
171 							     tmp_gicopts,
172 							     options);
173 		} else {
174 			/* Try library defaults. */
175 			tmp_gicopts = NULL;
176 		}
177 
178 		/* Display password help text. */
179 		if ((options->pwhelp != NULL) && (options->pwhelp[0] != '\0')) {
180 			fp = fopen(options->pwhelp, "r");
181 			if (fp != NULL) {
182 				if (options->debug) {
183 					debug("opened help file '%s'",
184 					      options->pwhelp);
185 				}
186 				if (fstat(fileno(fp), &st) != -1) {
187 					pwhelp = malloc(st.st_size + 1);
188 					if (pwhelp == NULL) {
189 						memset(prompt, '\0',
190 						       sizeof(prompt));
191 						i = fread(prompt, 1,
192 							  sizeof(prompt) -1,
193 							  fp);
194 						pwhelp = prompt;
195 					} else {
196 						memset(pwhelp, '\0',
197 						       st.st_size + 1);
198 						i = fread(pwhelp, 1,
199 							  st.st_size, fp);
200 						if (options->debug) {
201 							debug("read %d bytes",
202 							      (int) st.st_size);
203 						}
204 					}
205 				} else {
206 					memset(prompt, '\0', sizeof(prompt));
207 					i = fread(prompt, 1,
208 						  sizeof(prompt) - 1, fp);
209 					pwhelp = prompt;
210 				}
211 				if (i > 0) {
212 					message.msg = pwhelp;
213 					message.msg_style = PAM_TEXT_INFO;
214 					_pam_krb5_conv_call(pamh, &message, 1,
215 							    NULL);
216 				}
217 				if (pwhelp != prompt) {
218 					xstrfree(pwhelp);
219 				}
220 				fclose(fp);
221 			} else {
222 				if (options->debug) {
223 					debug("failed to open help file '%s'",
224 					      options->pwhelp);
225 				}
226 			}
227 		}
228 
229 		/* Obtain the current password. */
230 		password = NULL;
231 		if (options->use_first_pass) {
232 			/* Read the stored password. */
233 			password = NULL;
234 			i = _pam_krb5_get_item_text(pamh, PAM_OLDAUTHTOK,
235 						    &password);
236 			/* Duplicate the password so that we can free it later
237 			 * without corrupting the heap. */
238 			if ((password != NULL) && (i == PAM_SUCCESS)) {
239 				password = xstrdup(password);
240 			}
241 		}
242 		if ((password != NULL) && (i == PAM_SUCCESS)) {
243 			if (options->debug) {
244 				if (use_third_pass) {
245 					debug("trying previously-entered "
246 					      "password for '%s', allowing "
247 					      "libkrb5 to prompt for more",
248 					      user);
249 				} else {
250 					debug("trying previously-entered "
251 					      "password for '%s'", user);
252 				}
253 			}
254 			/* We have a password, so try to obtain initial
255 			 * credentials using the password. */
256 			i = v5_get_creds(stash->v5ctx, pamh,
257 					 &stash->v5ccache,
258 					 &stash->v5armorccache,
259 					 user, userinfo,
260 					 options,
261 					 PASSWORD_CHANGE_PRINCIPAL,
262 					 password, tmp_gicopts,
263 					 use_third_pass ?
264 					 _pam_krb5_normal_prompter :
265 					 _pam_krb5_previous_prompter,
266 					 NULL,
267 					 &tmp_result);
268 			prelim_attempted = 1;
269 			use_third_pass = 0;
270 			if (options->debug) {
271 				debug("Got %d (%s) acquiring credentials for "
272 				      "%s: %s.",
273 				      tmp_result, v5_error_message(tmp_result),
274 				      PASSWORD_CHANGE_PRINCIPAL,
275 				      pam_strerror(pamh, i));
276 			}
277 			if (i != PAM_SUCCESS) {
278 				/* No joy. */
279 				xstrfree(password);
280 				password = NULL;
281 			}
282 			retval = i;
283 		}
284 		if ((retval != PAM_SUCCESS) &&
285 		    (password == NULL) &&
286 		    options->use_second_pass) {
287 			/* Ask the user for a password. */
288 			sprintf(prompt, Y_("%s%sPassword: "),
289 				options->banner,
290 				strlen(options->banner) > 0 ? " " : "");
291 			i = _pam_krb5_prompt_for(pamh, prompt, &password);
292 			/* Save the old password for possible use by other
293 			 * modules. */
294 			if ((password != NULL) && (i == PAM_SUCCESS)) {
295 				pam_set_item(pamh, PAM_OLDAUTHTOK, password);
296 			}
297 		}
298 		/* We have a second password, so try to obtain initial
299 		 * credentials using the password. */
300 		if ((retval != PAM_SUCCESS) &&
301 		    ((password != NULL) && (i == PAM_SUCCESS))) {
302 			if (options->debug) {
303 				if (use_third_pass) {
304 					debug("trying newly-entered "
305 					      "password for '%s', allowing "
306 					      "libkrb5 to prompt for more",
307 					      user);
308 				} else {
309 					debug("trying newly-entered "
310 					      "password for '%s'", user);
311 				}
312 			}
313 			i = v5_get_creds(stash->v5ctx, pamh,
314 					 &stash->v5ccache,
315 					 &stash->v5armorccache,
316 					 user, userinfo,
317 					 options,
318 					 PASSWORD_CHANGE_PRINCIPAL,
319 					 password, tmp_gicopts,
320 					 use_third_pass ?
321 					 _pam_krb5_normal_prompter :
322 					 _pam_krb5_always_fail_prompter,
323 					 NULL,
324 					 &tmp_result);
325 			prelim_attempted = 1;
326 			use_third_pass = 0;
327 			if (options->debug) {
328 				debug("Got %d (%s) acquiring credentials for "
329 				      "%s.",
330 				      tmp_result, v5_error_message(tmp_result),
331 				      PASSWORD_CHANGE_PRINCIPAL);
332 			}
333 			retval = i;
334 		}
335 		/* We haven't tried anything yet, so if it's allowed, try to
336 		 * obtain initial credentials, letting libkrb5 ask the
337 		 * questions. */
338 		if ((retval != PAM_SUCCESS) &&
339 		    (prelim_attempted == 0) &&
340 		    use_third_pass) {
341 			if (options->debug) {
342 				debug("not using an entered password for '%s', "
343 				      "allowing libkrb5 to prompt", user);
344 			}
345 			i = v5_get_creds(stash->v5ctx, pamh,
346 					 &stash->v5ccache,
347 					 &stash->v5armorccache,
348 					 user, userinfo,
349 					 options,
350 					 PASSWORD_CHANGE_PRINCIPAL,
351 					 NULL, tmp_gicopts,
352 					 options->permit_password_callback ?
353 					 _pam_krb5_always_prompter :
354 					 _pam_krb5_normal_prompter,
355 					 NULL,
356 					 &tmp_result);
357 			prelim_attempted = 1;
358 			use_third_pass = 0;
359 			if (options->debug) {
360 				debug("Got %d (%s) acquiring credentials for "
361 				      "%s.",
362 				      tmp_result, v5_error_message(tmp_result),
363 				      PASSWORD_CHANGE_PRINCIPAL);
364 			}
365 			retval = i;
366 		}
367 
368 		/* Clean up the password-changing options. */
369 		v5_free_get_init_creds_opt(ctx, tmp_gicopts);
370 		/* Free [the copy of] the password. */
371 		xstrfree(password);
372 	}
373 
374 	/* If this is the second pass, get the new password, use the
375 	 * credentials which we obtained and stashed in the first pass to set
376 	 * the user's password, and then use the new password to obtain a TGT.
377 	 * (If we're changing an expired password, then we'll need it to create
378 	 * a ccache file later.) */
379 	if (flags & PAM_UPDATE_AUTHTOK) {
380 		retval = PAM_AUTHTOK_ERR;
381 		password = NULL;
382 
383 		/* If our preliminary check succeeded, then we'll have
384 		 * password-changing credentials. */
385 		if (v5_ccache_has_pwc(ctx, stash->v5ccache, NULL) == 0) {
386 			/* The new password (if it's already been requested by
387 			 * a previously-called module) is stored as the
388 			 * PAM_AUTHTOK item.  The old one is stored as the
389 			 * PAM_OLDAUTHTOK item, but we don't use it here. */
390 			i = _pam_krb5_get_item_text(pamh, PAM_AUTHTOK,
391 						    &password);
392 
393 			/* Duplicate the password, as above. */
394 			if ((password != NULL) && (i == PAM_SUCCESS)) {
395 				password = xstrdup(password);
396 			} else {
397 				/* Indicate that we didn't get a satisfactory
398 				 * password, but we can ask the user. */
399 				retval = PAM_AUTHTOK_ERR;
400 				password = NULL;
401 			}
402 
403 			/* If there wasn't a previously-entered password, and
404 			 * we can't ask the user, then return an error. */
405 			if ((password == NULL) && (options->use_authtok)) {
406 				retval = PAM_AUTHTOK_RECOVERY_ERR;
407 			}
408 		} else {
409 			/* If we didn't pass the preliminary check, then stand
410 			 * back and let whichever module it was that told the
411 			 * calling application it was okay to continue to do
412 			 * its thing. */
413 			if (options->ignore_unknown_principals) {
414 				debug("no password-changing credentials for "
415 				      "'%s' obtained, ignoring user",
416 				      userinfo->unparsed_name);
417 				retval = PAM_IGNORE;
418 			} else {
419 				debug("no password-changing credentials for "
420 				      "'%s' obtained, user not known",
421 				      userinfo->unparsed_name);
422 				retval = PAM_USER_UNKNOWN;
423 			}
424 		}
425 
426 		/* If there wasn't a previously-entered password, and we are
427 		 * okay with that, ask for one. */
428 		if ((password == NULL) && (retval == PAM_AUTHTOK_ERR)) {
429 			/* Ask for the new password twice. */
430 			sprintf(prompt, Y_("New %s%sPassword: "),
431 				options->banner,
432 				strlen(options->banner) > 0 ? " " : "");
433 			sprintf(prompt2, Y_("Repeat New %s%sPassword: "),
434 				options->banner,
435 				strlen(options->banner) > 0 ? " " : "");
436 			i = _pam_krb5_prompt_for_2(pamh, prompt, &password,
437 						   prompt2, &password2);
438 			/* If they're not the same, return PAM_TRY_AGAIN. */
439 			if (strcmp(password, password2) != 0) {
440 				i = PAM_TRY_AGAIN;
441 				retval = PAM_TRY_AGAIN;
442 			}
443 			/* Save the password for possible use by other
444 			 * modules. */
445 			if (i == PAM_SUCCESS) {
446 				pam_set_item(pamh, PAM_AUTHTOK, password);
447 			}
448 			/* Free the second password, we only need one copy. */
449 			xstrfree(password2);
450 			password2 = NULL;
451 		}
452 
453 		/* We have the new password, so attempt to change the user's
454 		 * password using the previously-acquired password-changing
455 		 * ticket. */
456 		memset(&pwc_creds, 0, sizeof(pwc_creds));
457 		if ((password != NULL) &&
458 		    (retval == PAM_AUTHTOK_ERR) &&
459 		    (v5_ccache_has_pwc(ctx, stash->v5ccache, &pwc_creds) == 0)) {
460 			int result_code;
461 			krb5_data result_code_string, result_string;
462 			result_code = -1;
463 			result_string.length = 0;
464 			result_string.data = NULL;
465 			result_code_string.length = 0;
466 			result_code_string.data = NULL;
467 			i = v5_change_password(ctx, &pwc_creds, password,
468 					       &result_code,
469 					       &result_code_string,
470 					       &result_string);
471 			krb5_free_cred_contents(ctx, &pwc_creds);
472 			if ((i == 0) && (result_code == 0)) {
473 				notice("password changed for %s",
474 				       userinfo->unparsed_name);
475 				retval = PAM_SUCCESS;
476 			} else {
477 				if (i != 0) {
478 					notice("password change failed for "
479 					       "%s: %s",
480 					       userinfo->unparsed_name,
481 					       v5_error_message(i));
482 				} else {
483 					notice("password change failed for "
484 					       "%s: %s: %.*s %s%.*s%s",
485 					       userinfo->unparsed_name,
486 					       v5_passwd_error_message(result_code),
487 					       (int) result_code_string.length,
488 					       (char *) result_code_string.data,
489 					       result_string.length ? "(" : "",
490 					       (int) result_string.length,
491 					       (char *) result_string.data,
492 					       result_string.length ? ")" : "");
493 				}
494 				if ((result_string.length > 0) ||
495 				    (result_code_string.length > 0)) {
496 					notice_user(pamh, "%s: %.*s %s%.*s%s\n",
497 						    v5_passwd_error_message(result_code),
498 						    (int) result_code_string.length,
499 						    (const char *) result_code_string.data,
500 						    result_string.length ?
501 						    "(" : "",
502 						    (int) result_string.length,
503 						    (const char *) result_string.data,
504 						    result_string.length ?
505 						    ")" : "");
506 				}
507 			}
508 		}
509 
510 		/* If we succeeded, obtain a new TGT using the new password. */
511 		if (retval == PAM_SUCCESS) {
512 			if (options->debug) {
513 				debug("obtaining credentials using new "
514 				      "password for '%s'",
515 				      userinfo->unparsed_name);
516 			}
517 			i = v5_get_creds(stash->v5ctx, pamh, &stash->v5ccache,
518 					 &stash->v5armorccache,
519 					 user, userinfo, options,
520 					 KRB5_TGS_NAME,
521 					 password, gic_options,
522 					 _pam_krb5_always_fail_prompter,
523 					 NULL,
524 					 &stash->v5result);
525 			stash->v5attempted = 1;
526 			if (i == PAM_SUCCESS) {
527 				if (options->use_shmem) {
528 					_pam_krb5_stash_shm_write(pamh, stash,
529 								  options,
530 								  user, userinfo);
531 				}
532 			}
533 		}
534 
535 		/* Free the new password. */
536 		if (password != NULL) {
537 			xstrfree(password);
538 		}
539 	}
540 
541 	/* Clean up. */
542 	if (options->debug) {
543 		debug("pam_chauthtok (%s) returning %d (%s)",
544 		      (flags & PAM_PRELIM_CHECK) ?
545 		      "preliminary check" :
546 		      ((flags & PAM_UPDATE_AUTHTOK) ?
547 		       "updating authtok":
548 		       "unknown phase"),
549 		      retval, pam_strerror(pamh, retval));
550 	}
551 	_pam_krb5_user_info_free(ctx, userinfo);
552 	_pam_krb5_options_free(pamh, ctx, options);
553 	v5_free_get_init_creds_opt(ctx, gic_options);
554 	_pam_krb5_free_ctx(ctx);
555 	return retval;
556 }
557