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