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