1 /*
2  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 
7 #include "k5-int.h"
8 #include "com_err.h"
9 #include <admin.h>
10 #include <locale.h>
11 #include <syslog.h>
12 
13 /* Solaris Kerberos:
14  *
15  * Change Password functionality is handled by the libkadm5clnt.so.1 library in
16  * Solaris Kerberos. In order to avoid a circular dependency between that lib
17  * and the kerberos mech lib, we use the #pragma weak compiler directive.
18  * This way, when applications link with the libkadm5clnt.so.1 lib the circular
19  * dependancy between the two libs will be resolved.
20  */
21 
22 #pragma weak kadm5_get_cpw_host_srv_name
23 #pragma weak kadm5_init_with_password
24 #pragma weak kadm5_chpass_principal_util
25 
26 extern kadm5_ret_t kadm5_get_cpw_host_srv_name(krb5_context, const char *,
27 			char **);
28 extern kadm5_ret_t kadm5_init_with_password(char *, char *, char *,
29 			kadm5_config_params *, krb5_ui_4, krb5_ui_4, char **,
30 			void **);
31 extern kadm5_ret_t kadm5_chpass_principal_util(void *, krb5_principal,
32 			char *, char **, char *, unsigned int);
33 
34 /*
35  * Solaris Kerberos:
36  * See the function's definition for the description of this interface.
37  */
38 krb5_error_code __krb5_get_init_creds_password(krb5_context,
39 	krb5_creds *, krb5_principal, char *, krb5_prompter_fct, void *,
40 	krb5_deltat, char *, krb5_get_init_creds_opt *, krb5_kdc_rep **);
41 
42 static krb5_error_code
43 krb5_get_as_key_password(
44     krb5_context context,
45     krb5_principal client,
46     krb5_enctype etype,
47     krb5_prompter_fct prompter,
48     void *prompter_data,
49     krb5_data *salt,
50     krb5_data *params,
51     krb5_keyblock *as_key,
52     void *gak_data)
53 {
54     krb5_data *password;
55     krb5_error_code ret;
56     krb5_data defsalt;
57     char *clientstr;
58     char promptstr[1024];
59     krb5_prompt prompt;
60     krb5_prompt_type prompt_type;
61 
62     password = (krb5_data *) gak_data;
63 
64     /* If there's already a key of the correct etype, we're done.
65        If the etype is wrong, free the existing key, and make
66        a new one.
67 
68        XXX This was the old behavior, and was wrong in hw preauth
69        cases.  Is this new behavior -- always asking -- correct in all
70        cases?  */
71 
72     if (as_key->length) {
73 	if (as_key->enctype != etype) {
74 	    krb5_free_keyblock_contents (context, as_key);
75 	    as_key->length = 0;
76 	}
77     }
78 
79     if (password->data[0] == '\0') {
80 	if (prompter == NULL)
81 		prompter = krb5_prompter_posix; /* Solaris Kerberos */
82 
83 	if ((ret = krb5_unparse_name(context, client, &clientstr)))
84 	  return(ret);
85 
86 	strcpy(promptstr, "Password for ");
87 	strncat(promptstr, clientstr, sizeof(promptstr)-strlen(promptstr)-1);
88 	promptstr[sizeof(promptstr)-1] = '\0';
89 
90 	free(clientstr);
91 
92 	prompt.prompt = promptstr;
93 	prompt.hidden = 1;
94 	prompt.reply = password;
95 	prompt_type = KRB5_PROMPT_TYPE_PASSWORD;
96 
97 	/* PROMPTER_INVOCATION */
98 	krb5int_set_prompt_types(context, &prompt_type);
99 	if ((ret = (((*prompter)(context, prompter_data, NULL, NULL,
100 				1, &prompt))))) {
101 	    krb5int_set_prompt_types(context, 0);
102 	    return(ret);
103 	}
104 	krb5int_set_prompt_types(context, 0);
105     }
106 
107     if ((salt->length == -1 || salt->length == SALT_TYPE_AFS_LENGTH) && (salt->data == NULL)) {
108 	if ((ret = krb5_principal2salt(context, client, &defsalt)))
109 	    return(ret);
110 
111 	salt = &defsalt;
112     } else {
113 	defsalt.length = 0;
114     }
115 
116     ret = krb5_c_string_to_key_with_params(context, etype, password, salt,
117 					   params->data?params:NULL, as_key);
118 
119     if (defsalt.length)
120 	krb5_xfree(defsalt.data);
121 
122     return(ret);
123 }
124 
125 krb5_error_code KRB5_CALLCONV
126 krb5_get_init_creds_password(krb5_context context,
127 			     krb5_creds *creds,
128 			     krb5_principal client,
129 			     char *password,
130 			     krb5_prompter_fct prompter,
131 			     void *data,
132 			     krb5_deltat start_time,
133 			     char *in_tkt_service,
134 			     krb5_get_init_creds_opt *options)
135 {
136 	/*
137 	 * Solaris Kerberos:
138 	 * We call our own private function that returns the as_reply back to
139 	 * the caller.  This structure contains information, such as
140 	 * key-expiration and last-req fields.  Entities such as pam_krb5 can
141 	 * use this information to provide account/password expiration warnings.
142 	 * The original "prompter" interface is not granular enough for PAM,
143 	 * as it will perform all passes w/o coordination with other modules.
144 	 */
145 	return (__krb5_get_init_creds_password(context, creds, client, password,
146 		prompter, data, start_time, in_tkt_service, options, NULL));
147 }
148 
149 /*
150  * Solaris Kerberos:
151  * See krb5_get_init_creds_password()'s comments for the justification of this
152  * private function.  Caller must free ptr_as_reply if non-NULL.
153  */
154 krb5_error_code KRB5_CALLCONV
155 __krb5_get_init_creds_password(
156      krb5_context context,
157      krb5_creds *creds,
158      krb5_principal client,
159      char *password,
160      krb5_prompter_fct prompter,
161      void *data,
162      krb5_deltat start_time,
163      char *in_tkt_service,
164      krb5_get_init_creds_opt *options,
165      krb5_kdc_rep **ptr_as_reply)
166 {
167    krb5_error_code ret, ret2;
168    int use_master;
169    krb5_kdc_rep *as_reply;
170    int tries;
171    krb5_creds chpw_creds;
172    krb5_data pw0, pw1;
173    char banner[1024], pw0array[1024], pw1array[1024];
174    krb5_prompt prompt[2];
175    krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
176    krb5_gic_opt_ext *opte = NULL;
177    krb5_gic_opt_ext *chpw_opte = NULL;
178 
179    char admin_realm[1024], *cpw_service=NULL, *princ_str=NULL;
180    kadm5_config_params  params;
181    void *server_handle;
182 
183    use_master = 0;
184    as_reply = NULL;
185    memset(&chpw_creds, 0, sizeof(chpw_creds));
186 
187    pw0.data = pw0array;
188 
189    if (password && password[0]) {
190       if ((pw0.length = strlen(password)) > sizeof(pw0array)) {
191 	 ret = EINVAL;
192 	 goto cleanup;
193       }
194       strcpy(pw0.data, password);
195    } else {
196       pw0.data[0] = '\0';
197       pw0.length = sizeof(pw0array);
198    }
199 
200    pw1.data = pw1array;
201    pw1.data[0] = '\0';
202    pw1.length = sizeof(pw1array);
203 
204    ret = krb5int_gic_opt_to_opte(context, options, &opte, 1,
205 				 "krb5_get_init_creds_password");
206    if (ret)
207       goto cleanup;
208 
209    /* first try: get the requested tkt from any kdc */
210 
211    ret = krb5_get_init_creds(context, creds, client, prompter, data,
212 			     start_time, in_tkt_service, opte,
213 			     krb5_get_as_key_password, (void *) &pw0,
214 			     &use_master, &as_reply);
215 
216    /* check for success */
217 
218    if (ret == 0)
219       goto cleanup;
220 
221    /* If all the kdc's are unavailable, or if the error was due to a
222       user interrupt, or preauth errored out, fail */
223 
224    if ((ret == KRB5_KDC_UNREACH) ||
225        (ret == KRB5_PREAUTH_FAILED) ||
226        (ret == KRB5_LIBOS_PWDINTR) ||
227 	   (ret == KRB5_REALM_CANT_RESOLVE))
228       goto cleanup;
229 
230    /* if the reply did not come from the master kdc, try again with
231       the master kdc */
232 
233    if (!use_master) {
234       use_master = 1;
235 
236       if (as_reply) {
237 	  krb5_free_kdc_rep( context, as_reply);
238 	  as_reply = NULL;
239       }
240       ret2 = krb5_get_init_creds(context, creds, client, prompter, data,
241 				 start_time, in_tkt_service, opte,
242 				 krb5_get_as_key_password, (void *) &pw0,
243 				 &use_master, &as_reply);
244 
245       if (ret2 == 0) {
246 	 ret = 0;
247 	 goto cleanup;
248       }
249 
250       /* if the master is unreachable, return the error from the
251 	 slave we were able to contact or reset the use_master flag */
252 
253        if ((ret2 != KRB5_KDC_UNREACH) &&
254 	    (ret2 != KRB5_REALM_CANT_RESOLVE) &&
255 	    (ret2 != KRB5_REALM_UNKNOWN))
256 	   ret = ret2;
257        else
258 	   use_master = 0;
259    }
260 
261 /* Solaris Kerberos: 163 resync */
262 /* #ifdef USE_LOGIN_LIBRARY */
263 	if (ret == KRB5KDC_ERR_KEY_EXP)
264 		goto cleanup;	/* Login library will deal appropriately with this error */
265 /* #endif */
266 
267    /* at this point, we have an error from the master.  if the error
268       is not password expired, or if it is but there's no prompter,
269       return this error */
270 
271    if ((ret != KRB5KDC_ERR_KEY_EXP) ||
272        (prompter == NULL))
273       goto cleanup;
274 
275     /* historically the default has been to prompt for password change.
276      * if the change password prompt option has not been set, we continue
277      * to prompt.  Prompting is only disabled if the option has been set
278      * and the value has been set to false.
279      */
280     if (!(options->flags & KRB5_GET_INIT_CREDS_OPT_CHG_PWD_PRMPT))
281 	goto cleanup;
282 
283     /* ok, we have an expired password.  Give the user a few chances
284       to change it */
285 
286 
287    /*
288     * Solaris Kerberos:
289     * Get the correct change password service principal name to use.
290     * This is necessary because SEAM based admin servers require
291     * a slightly different service principal name than MIT/MS servers.
292     */
293 
294    memset((char *) &params, 0, sizeof (params));
295 
296    snprintf(admin_realm, sizeof (admin_realm),
297 	krb5_princ_realm(context, client)->data);
298    params.mask |= KADM5_CONFIG_REALM;
299    params.realm = admin_realm;
300 
301    ret=kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service);
302 
303    if (ret != KADM5_OK) {
304 	syslog(LOG_ERR, dgettext(TEXT_DOMAIN,
305 	    "Kerberos mechanism library: Unable to get change password "
306 	    "service name for realm %s\n"), admin_realm);
307 	goto cleanup;
308    } else {
309 	ret=0;
310    }
311 
312    /* extract the string version of the principal */
313    if ((ret = krb5_unparse_name(context, client, &princ_str)))
314 	goto cleanup;
315 
316    ret = kadm5_init_with_password(princ_str, pw0array, cpw_service,
317 	&params, KADM5_STRUCT_VERSION, KADM5_API_VERSION_2, NULL,
318 	&server_handle);
319 
320    if (ret != 0) {
321 	goto cleanup;
322    }
323 
324    prompt[0].prompt = "Enter new password";
325    prompt[0].hidden = 1;
326    prompt[0].reply = &pw0;
327    prompt_types[0] = KRB5_PROMPT_TYPE_NEW_PASSWORD;
328 
329    prompt[1].prompt = "Enter it again";
330    prompt[1].hidden = 1;
331    prompt[1].reply = &pw1;
332    prompt_types[1] = KRB5_PROMPT_TYPE_NEW_PASSWORD_AGAIN;
333 
334    strcpy(banner, "Password expired.  You must change it now.");
335 
336    for (tries = 3; tries; tries--) {
337       pw0.length = sizeof(pw0array);
338       pw1.length = sizeof(pw1array);
339 
340       /* PROMPTER_INVOCATION */
341       krb5int_set_prompt_types(context, prompt_types);
342       if ((ret = ((*prompter)(context, data, 0, banner,
343 			      sizeof(prompt)/sizeof(prompt[0]), prompt))))
344 	 goto cleanup;
345       krb5int_set_prompt_types(context, 0);
346 
347 
348       if (strcmp(pw0.data, pw1.data) != 0) {
349 	 ret = KRB5_LIBOS_BADPWDMATCH;
350 	 sprintf(banner, "%s.  Please try again.", error_message(ret));
351       } else if (pw0.length == 0) {
352 	 ret = KRB5_CHPW_PWDNULL;
353 	 sprintf(banner, "%s.  Please try again.", error_message(ret));
354       } else {
355 	 int result_code;
356 	 krb5_data code_string;
357 	 krb5_data result_string;
358 
359 	 if ((ret = krb5_change_password(context, &chpw_creds, pw0array,
360 					 &result_code, &code_string,
361 					 &result_string)))
362 	    goto cleanup;
363 
364 	 /* the change succeeded.  go on */
365 
366 	 if (result_code == 0) {
367 	    krb5_xfree(result_string.data);
368 	    break;
369 	 }
370 
371 	 /* set this in case the retry loop falls through */
372 
373 	 ret = KRB5_CHPW_FAIL;
374 
375 	 if (result_code != KRB5_KPASSWD_SOFTERROR) {
376 	    krb5_xfree(result_string.data);
377 	    goto cleanup;
378 	 }
379 
380 	 /* the error was soft, so try again */
381 
382 	 /* 100 is I happen to know that no code_string will be longer
383 	    than 100 chars */
384 
385 	 if (result_string.length > (sizeof(banner)-100))
386 	    result_string.length = sizeof(banner)-100;
387 
388 	 sprintf(banner, "%.*s%s%.*s.  Please try again.\n",
389 		 (int) code_string.length, code_string.data,
390 		 result_string.length ? ": " : "",
391 		 (int) result_string.length,
392 		 result_string.data ? result_string.data : "");
393 
394 	 krb5_xfree(code_string.data);
395 	 krb5_xfree(result_string.data);
396       }
397    }
398 
399    if (ret)
400       goto cleanup;
401 
402    /* the password change was successful.  Get an initial ticket
403       from the master.  this is the last try.  the return from this
404       is final.  */
405 
406    ret = krb5_get_init_creds(context, creds, client, prompter, data,
407 			     start_time, in_tkt_service, opte,
408 			     krb5_get_as_key_password, (void *) &pw0,
409 			     &use_master, &as_reply);
410 
411 cleanup:
412    krb5int_set_prompt_types(context, 0);
413    /* if getting the password was successful, then check to see if the
414       password is about to expire, and warn if so */
415 
416    if (ret == 0) {
417       krb5_timestamp now;
418       krb5_last_req_entry **last_req;
419       int hours;
420 
421       /* XXX 7 days should be configurable.  This is all pretty ad hoc,
422 	 and could probably be improved if I was willing to screw around
423 	 with timezones, etc. */
424 
425       if (prompter &&
426 	  (in_tkt_service && cpw_service &&
427 	   (strcmp(in_tkt_service, cpw_service) != 0)) &&
428 	  ((ret = krb5_timeofday(context, &now)) == 0) &&
429 	  as_reply->enc_part2->key_exp &&
430 	  ((hours = ((as_reply->enc_part2->key_exp-now)/(60*60))) <= 7*24) &&
431 	  (hours >= 0)) {
432 	 if (hours < 1)
433 	    sprintf(banner,
434 		    "Warning: Your password will expire in less than one hour.");
435 	 else if (hours <= 48)
436 	    sprintf(banner, "Warning: Your password will expire in %d hour%s.",
437 		    hours, (hours == 1)?"":"s");
438 	 else
439 	    sprintf(banner, "Warning: Your password will expire in %d days.",
440 		    hours/24);
441 
442 	 /* ignore an error here */
443          /* PROMPTER_INVOCATION */
444 	 (*prompter)(context, data, 0, banner, 0, 0);
445       } else if (prompter &&
446 		 (!in_tkt_service ||
447 		  (strcmp(in_tkt_service, "kadmin/changepw") != 0)) &&
448 		 as_reply->enc_part2 && as_reply->enc_part2->last_req) {
449 	 /*
450 	  * Check the last_req fields
451 	  */
452 
453 	 for (last_req = as_reply->enc_part2->last_req; *last_req; last_req++)
454 	    if ((*last_req)->lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
455 		(*last_req)->lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
456 	       krb5_deltat delta;
457 	       char ts[256];
458 
459 	       if ((ret = krb5_timeofday(context, &now)))
460 		  break;
461 
462 	       if ((ret = krb5_timestamp_to_string((*last_req)->value,
463 						   ts, sizeof(ts))))
464 		  break;
465 
466 	       delta = (*last_req)->value - now;
467 
468 	       if (delta < 3600)
469 		  sprintf(banner,
470 		    "Warning: Your password will expire in less than one "
471 		     "hour on %s", ts);
472 	       else if (delta < 86400*2)
473 		  sprintf(banner,
474 		     "Warning: Your password will expire in %d hour%s on %s",
475 		     delta / 3600, delta < 7200 ? "" : "s", ts);
476 	       else
477 		  sprintf(banner,
478 		     "Warning: Your password will expire in %d days on %s",
479 		     delta / 86400, ts);
480 	       /* ignore an error here */
481 	       /* PROMPTER_INVOCATION */
482 	       (*prompter)(context, data, 0, banner, 0, 0);
483 	    }
484       }
485    }
486 
487    free(cpw_service);
488    free(princ_str);
489    if (opte && krb5_gic_opt_is_shadowed(opte))
490       krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
491    memset(pw0array, 0, sizeof(pw0array));
492    memset(pw1array, 0, sizeof(pw1array));
493    krb5_free_cred_contents(context, &chpw_creds);
494    /*
495     * Solaris Kerberos:
496     * Argument, ptr_as_reply, being returned to caller if success and non-NULL.
497     */
498    if (as_reply != NULL) {
499 	if (ptr_as_reply == NULL)
500       	   krb5_free_kdc_rep(context, as_reply);
501 	else
502 	   *ptr_as_reply = as_reply;
503    }
504 
505    return(ret);
506 }
507 krb5_error_code krb5int_populate_gic_opt (
508     krb5_context context, krb5_gic_opt_ext **opte,
509     krb5_flags options, krb5_address * const *addrs, krb5_enctype *ktypes,
510     krb5_preauthtype *pre_auth_types, krb5_creds *creds)
511 {
512   int i;
513   krb5_int32 starttime;
514   krb5_get_init_creds_opt *opt;
515 
516 
517     krb5_get_init_creds_opt_init(opt);
518     if (addrs)
519       krb5_get_init_creds_opt_set_address_list(opt, (krb5_address **) addrs);
520     if (ktypes) {
521 	for (i=0; ktypes[i]; i++);
522 	if (i)
523 	    krb5_get_init_creds_opt_set_etype_list(opt, ktypes, i);
524     }
525     if (pre_auth_types) {
526 	for (i=0; pre_auth_types[i]; i++);
527 	if (i)
528 	    krb5_get_init_creds_opt_set_preauth_list(opt, pre_auth_types, i);
529     }
530     if (options&KDC_OPT_FORWARDABLE)
531 	krb5_get_init_creds_opt_set_forwardable(opt, 1);
532     else krb5_get_init_creds_opt_set_forwardable(opt, 0);
533     if (options&KDC_OPT_PROXIABLE)
534 	krb5_get_init_creds_opt_set_proxiable(opt, 1);
535     else krb5_get_init_creds_opt_set_proxiable(opt, 0);
536     if (creds && creds->times.endtime) {
537         krb5_timeofday(context, &starttime);
538         if (creds->times.starttime) starttime = creds->times.starttime;
539         krb5_get_init_creds_opt_set_tkt_life(opt, creds->times.endtime - starttime);
540     }
541     return krb5int_gic_opt_to_opte(context, opt, opte, 0,
542 				   "krb5int_populate_gic_opt");
543 }
544 
545 /*
546   Rewrites get_in_tkt in terms of newer get_init_creds API.
547  Attempts to get an initial ticket for creds->client to use server
548  creds->server, (realm is taken from creds->client), with options
549  options, and using creds->times.starttime, creds->times.endtime,
550  creds->times.renew_till as from, till, and rtime.
551  creds->times.renew_till is ignored unless the RENEWABLE option is requested.
552 
553  If addrs is non-NULL, it is used for the addresses requested.  If it is
554  null, the system standard addresses are used.
555 
556  If password is non-NULL, it is converted using the cryptosystem entry
557  point for a string conversion routine, seeded with the client's name.
558  If password is passed as NULL, the password is read from the terminal,
559  and then converted into a key.
560 
561  A succesful call will place the ticket in the credentials cache ccache.
562 
563  returns system errors, encryption errors
564  */
565 krb5_error_code KRB5_CALLCONV
566 krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options,
567 			      krb5_address *const *addrs, krb5_enctype *ktypes,
568 			      krb5_preauthtype *pre_auth_types,
569 			      const char *password, krb5_ccache ccache,
570 			      krb5_creds *creds, krb5_kdc_rep **ret_as_reply)
571 {
572     krb5_error_code retval;
573     krb5_data pw0;
574     char pw0array[1024];
575     char * server;
576     krb5_principal server_princ, client_princ;
577     int use_master = 0;
578     krb5_gic_opt_ext *opte = NULL;
579 
580     pw0array[0] = '\0';
581     pw0.data = pw0array;
582     if (password) {
583 	pw0.length = strlen(password);
584 	if (pw0.length > sizeof(pw0array))
585 	    return EINVAL;
586 	strncpy(pw0.data, password, sizeof(pw0array));
587 	if (pw0.length == 0)
588 	    pw0.length = sizeof(pw0array);
589     } else {
590 	pw0.length = sizeof(pw0array);
591     }
592     retval = krb5int_populate_gic_opt(context, &opte,
593 				      options, addrs, ktypes,
594 				      pre_auth_types, creds);
595     if (retval)
596       return (retval);
597     retval = krb5_unparse_name( context, creds->server, &server);
598     if (retval) {
599       return (retval);
600       krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
601     }
602     server_princ = creds->server;
603     client_princ = creds->client;
604         retval = krb5_get_init_creds (context,
605 					   creds, creds->client,
606 					   krb5_prompter_posix,  NULL,
607 					   0, server, opte,
608 				      krb5_get_as_key_password, &pw0,
609 				      &use_master, ret_as_reply);
610 	  krb5_free_unparsed_name( context, server);
611 	  krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
612 	if (retval) {
613 	  return (retval);
614 	}
615 	if (creds->server)
616 	    krb5_free_principal( context, creds->server);
617 	if (creds->client)
618 	    krb5_free_principal( context, creds->client);
619 	creds->client = client_princ;
620 	creds->server = server_princ;
621 	/* store it in the ccache! */
622 	if (ccache)
623 	  if ((retval = krb5_cc_store_cred(context, ccache, creds)))
624 	    return (retval);
625 	return retval;
626   }
627 
628