Imported Upstream version 1.17
[platform/upstream/krb5.git] / src / lib / krb5 / krb / gic_pwd.c
1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 #include "k5-int.h"
3 #include "com_err.h"
4 #include "init_creds_ctx.h"
5 #include "int-proto.h"
6 #include "os-proto.h"
7
8 krb5_error_code
9 krb5_get_as_key_password(krb5_context context,
10                          krb5_principal client,
11                          krb5_enctype etype,
12                          krb5_prompter_fct prompter,
13                          void *prompter_data,
14                          krb5_data *salt,
15                          krb5_data *params,
16                          krb5_keyblock *as_key,
17                          void *gak_data,
18                          k5_response_items *ritems)
19 {
20     struct gak_password *gp = gak_data;
21     krb5_error_code ret;
22     krb5_data defsalt;
23     char *clientstr;
24     char promptstr[1024], pwbuf[1024];
25     krb5_data pw;
26     krb5_prompt prompt;
27     krb5_prompt_type prompt_type;
28     const char *rpass;
29
30     /* If we need to get the AS key via the responder, ask for it. */
31     if (as_key == NULL) {
32         if (gp->password != NULL)
33             return 0;
34
35         return k5_response_items_ask_question(ritems,
36                                               KRB5_RESPONDER_QUESTION_PASSWORD,
37                                               "");
38     }
39
40     /* If there's already a key of the correct etype, we're done.
41        If the etype is wrong, free the existing key, and make
42        a new one.
43
44        XXX This was the old behavior, and was wrong in hw preauth
45        cases.  Is this new behavior -- always asking -- correct in all
46        cases?  */
47
48     if (as_key->length) {
49         if (as_key->enctype != etype) {
50             krb5_free_keyblock_contents (context, as_key);
51             as_key->length = 0;
52         }
53     }
54
55     if (gp->password == NULL) {
56         /* Check the responder for the password. */
57         rpass = k5_response_items_get_answer(ritems,
58                                              KRB5_RESPONDER_QUESTION_PASSWORD);
59         if (rpass != NULL) {
60             ret = alloc_data(&gp->storage, strlen(rpass));
61             if (ret)
62                 return ret;
63             memcpy(gp->storage.data, rpass, strlen(rpass));
64             gp->password = &gp->storage;
65         }
66     }
67
68     if (gp->password == NULL) {
69         if (prompter == NULL)
70             return(EIO);
71
72         if ((ret = krb5_unparse_name(context, client, &clientstr)))
73             return(ret);
74
75         snprintf(promptstr, sizeof(promptstr), _("Password for %s"),
76                  clientstr);
77         free(clientstr);
78
79         pw = make_data(pwbuf, sizeof(pwbuf));
80         prompt.prompt = promptstr;
81         prompt.hidden = 1;
82         prompt.reply = &pw;
83         prompt_type = KRB5_PROMPT_TYPE_PASSWORD;
84
85         /* PROMPTER_INVOCATION */
86         k5_set_prompt_types(context, &prompt_type);
87         ret = (*prompter)(context, prompter_data, NULL, NULL, 1, &prompt);
88         k5_set_prompt_types(context, 0);
89         if (ret)
90             return(ret);
91
92         ret = krb5int_copy_data_contents(context, &pw, &gp->storage);
93         zap(pw.data, pw.length);
94         if (ret)
95             return ret;
96         gp->password = &gp->storage;
97     }
98
99     if (salt == NULL) {
100         if ((ret = krb5_principal2salt(context, client, &defsalt)))
101             return(ret);
102
103         salt = &defsalt;
104     } else {
105         defsalt.length = 0;
106     }
107
108     ret = krb5_c_string_to_key_with_params(context, etype, gp->password, salt,
109                                            params->data?params:NULL, as_key);
110
111     if (defsalt.length)
112         free(defsalt.data);
113
114     return(ret);
115 }
116
117 krb5_error_code KRB5_CALLCONV
118 krb5_init_creds_set_password(krb5_context context,
119                              krb5_init_creds_context ctx,
120                              const char *password)
121 {
122     char *s;
123
124     s = strdup(password);
125     if (s == NULL)
126         return ENOMEM;
127
128     zapfree(ctx->gakpw.storage.data, ctx->gakpw.storage.length);
129     ctx->gakpw.storage = string2data(s);
130     ctx->gakpw.password = &ctx->gakpw.storage;
131     ctx->gak_fct = krb5_get_as_key_password;
132     ctx->gak_data = &ctx->gakpw;
133     return 0;
134 }
135
136 /* Return the password expiry time indicated by enc_part2.  Set *is_last_req
137  * if the information came from a last_req value. */
138 static void
139 get_expiry_times(krb5_enc_kdc_rep_part *enc_part2, krb5_timestamp *pw_exp,
140                  krb5_timestamp *acct_exp, krb5_boolean *is_last_req)
141 {
142     krb5_last_req_entry **last_req;
143     krb5_int32 lr_type;
144
145     *pw_exp = 0;
146     *acct_exp = 0;
147     *is_last_req = FALSE;
148
149     /* Look for last-req entries for password or account expiration. */
150     if (enc_part2->last_req) {
151         for (last_req = enc_part2->last_req; *last_req; last_req++) {
152             lr_type = (*last_req)->lr_type;
153             if (lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
154                 lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
155                 *is_last_req = TRUE;
156                 *pw_exp = (*last_req)->value;
157             } else if (lr_type == KRB5_LRQ_ALL_ACCT_EXPTIME ||
158                        lr_type == KRB5_LRQ_ONE_ACCT_EXPTIME) {
159                 *is_last_req = TRUE;
160                 *acct_exp = (*last_req)->value;
161             }
162         }
163     }
164
165     /* If we didn't find any, use the ambiguous key_exp field. */
166     if (*is_last_req == FALSE)
167         *pw_exp = enc_part2->key_exp;
168 }
169
170 /*
171  * Send an appropriate warning prompter if as_reply indicates that the password
172  * is going to expire soon.  If an expire callback was provided, use that
173  * instead.
174  */
175 static void
176 warn_pw_expiry(krb5_context context, krb5_get_init_creds_opt *options,
177                krb5_prompter_fct prompter, void *data,
178                const char *in_tkt_service, krb5_kdc_rep *as_reply)
179 {
180     krb5_error_code ret;
181     krb5_expire_callback_func expire_cb;
182     void *expire_data;
183     krb5_timestamp pw_exp, acct_exp, now;
184     krb5_boolean is_last_req;
185     krb5_deltat delta;
186     char ts[256], banner[1024];
187
188     get_expiry_times(as_reply->enc_part2, &pw_exp, &acct_exp, &is_last_req);
189
190     k5_gic_opt_get_expire_cb(options, &expire_cb, &expire_data);
191     if (expire_cb != NULL) {
192         /* Invoke the expire callback and don't send prompter warnings. */
193         (*expire_cb)(context, expire_data, pw_exp, acct_exp, is_last_req);
194         return;
195     }
196
197     /* Don't warn if no password expiry value was sent. */
198     if (pw_exp == 0)
199         return;
200
201     /* Don't warn if the password is being changed. */
202     if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0)
203         return;
204
205     /*
206      * If the expiry time came from a last_req field, assume the KDC wants us
207      * to warn.  Otherwise, warn only if the expiry time is less than a week
208      * from now.
209      */
210     ret = krb5_timeofday(context, &now);
211     if (ret != 0)
212         return;
213     if (!is_last_req &&
214         (ts_after(now, pw_exp) || ts_delta(pw_exp, now) > 7 * 24 * 60 * 60))
215         return;
216
217     if (!prompter)
218         return;
219
220     ret = krb5_timestamp_to_string(pw_exp, ts, sizeof(ts));
221     if (ret != 0)
222         return;
223
224     delta = ts_delta(pw_exp, now);
225     if (delta < 3600) {
226         snprintf(banner, sizeof(banner),
227                  _("Warning: Your password will expire in less than one hour "
228                    "on %s"), ts);
229     } else if (delta < 86400*2) {
230         snprintf(banner, sizeof(banner),
231                  _("Warning: Your password will expire in %d hour%s on %s"),
232                  delta / 3600, delta < 7200 ? "" : "s", ts);
233     } else {
234         snprintf(banner, sizeof(banner),
235                  _("Warning: Your password will expire in %d days on %s"),
236                  delta / 86400, ts);
237     }
238
239     /* PROMPTER_INVOCATION */
240     (*prompter)(context, data, 0, banner, 0, 0);
241 }
242
243 /*
244  * Create a temporary options structure for getting a kadmin/changepw ticket,
245  * based on the appplication-specified options.  Propagate all application
246  * options which affect preauthentication, but not options which affect the
247  * resulting ticket or how it is stored.  Set lifetime and flags appropriate
248  * for a ticket which we will use immediately and then discard.
249  *
250  * The caller should free the result with free().
251  */
252 static krb5_error_code
253 make_chpw_options(krb5_context context, krb5_get_init_creds_opt *in,
254                   krb5_get_init_creds_opt **out)
255 {
256     krb5_get_init_creds_opt *opt;
257
258     *out = NULL;
259     opt = k5_gic_opt_shallow_copy(in);
260     if (opt == NULL)
261         return ENOMEM;
262
263     /* Get a non-forwardable, non-proxiable, short-lifetime ticket. */
264     krb5_get_init_creds_opt_set_tkt_life(opt, 5 * 60);
265     krb5_get_init_creds_opt_set_renew_life(opt, 0);
266     krb5_get_init_creds_opt_set_forwardable(opt, 0);
267     krb5_get_init_creds_opt_set_proxiable(opt, 0);
268
269     /* Unset options which should only apply to the actual ticket. */
270     opt->flags &= ~KRB5_GET_INIT_CREDS_OPT_ADDRESS_LIST;
271     opt->flags &= ~KRB5_GET_INIT_CREDS_OPT_ANONYMOUS;
272
273     /* The output ccache should only be used for the actual ticket. */
274     krb5_get_init_creds_opt_set_out_ccache(context, opt, NULL);
275
276     *out = opt;
277     return 0;
278 }
279
280 krb5_error_code KRB5_CALLCONV
281 krb5_get_init_creds_password(krb5_context context,
282                              krb5_creds *creds,
283                              krb5_principal client,
284                              const char *password,
285                              krb5_prompter_fct prompter,
286                              void *data,
287                              krb5_deltat start_time,
288                              const char *in_tkt_service,
289                              krb5_get_init_creds_opt *options)
290 {
291     krb5_error_code ret;
292     int use_master;
293     krb5_kdc_rep *as_reply;
294     int tries;
295     krb5_creds chpw_creds;
296     krb5_get_init_creds_opt *chpw_opts = NULL;
297     struct gak_password gakpw;
298     krb5_data pw0, pw1;
299     char banner[1024], pw0array[1024], pw1array[1024];
300     krb5_prompt prompt[2];
301     krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
302     struct errinfo errsave = EMPTY_ERRINFO;
303     char *message;
304
305     use_master = 0;
306     as_reply = NULL;
307     memset(&chpw_creds, 0, sizeof(chpw_creds));
308     memset(&gakpw, 0, sizeof(gakpw));
309
310     if (password != NULL) {
311         pw0 = string2data((char *)password);
312         gakpw.password = &pw0;
313     }
314
315     /* first try: get the requested tkt from any kdc */
316
317     ret = k5_get_init_creds(context, creds, client, prompter, data, start_time,
318                             in_tkt_service, options, krb5_get_as_key_password,
319                             &gakpw, &use_master, &as_reply);
320
321     /* check for success */
322
323     if (ret == 0)
324         goto cleanup;
325
326     /* If all the kdc's are unavailable, or if the error was due to a
327        user interrupt, fail */
328
329     if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
330         ret == KRB5_LIBOS_PWDINTR || ret == KRB5_LIBOS_CANTREADPWD)
331         goto cleanup;
332
333     /* if the reply did not come from the master kdc, try again with
334        the master kdc */
335
336     if (!use_master) {
337         TRACE_GIC_PWD_MASTER(context);
338         use_master = 1;
339
340         k5_save_ctx_error(context, ret, &errsave);
341         if (as_reply) {
342             krb5_free_kdc_rep( context, as_reply);
343             as_reply = NULL;
344         }
345         ret = k5_get_init_creds(context, creds, client, prompter, data,
346                                 start_time, in_tkt_service, options,
347                                 krb5_get_as_key_password, &gakpw, &use_master,
348                                 &as_reply);
349
350         if (ret == 0)
351             goto cleanup;
352
353         /* If the master is unreachable, return the error from the replica we
354          * were able to contact and reset the use_master flag. */
355         if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
356             ret == KRB5_REALM_UNKNOWN) {
357             ret = k5_restore_ctx_error(context, &errsave);
358             use_master = 0;
359         }
360     }
361
362     /* at this point, we have an error from the master.  if the error
363        is not password expired, or if it is but there's no prompter,
364        return this error */
365
366     if ((ret != KRB5KDC_ERR_KEY_EXP) ||
367         (prompter == NULL))
368         goto cleanup;
369
370     /* historically the default has been to prompt for password change.
371      * if the change password prompt option has not been set, we continue
372      * to prompt.  Prompting is only disabled if the option has been set
373      * and the value has been set to false.
374      */
375     if (options && !(options->flags & KRB5_GET_INIT_CREDS_OPT_CHG_PWD_PRMPT))
376         goto cleanup;
377     TRACE_GIC_PWD_EXPIRED(context);
378
379     /* ok, we have an expired password.  Give the user a few chances
380        to change it */
381
382     ret = make_chpw_options(context, options, &chpw_opts);
383     if (ret)
384         goto cleanup;
385     ret = k5_get_init_creds(context, &chpw_creds, client, prompter, data,
386                             start_time, "kadmin/changepw", chpw_opts,
387                             krb5_get_as_key_password, &gakpw, &use_master,
388                             NULL);
389     if (ret)
390         goto cleanup;
391
392     pw0.data = pw0array;
393     pw0.data[0] = '\0';
394     pw0.length = sizeof(pw0array);
395     prompt[0].prompt = _("Enter new password");
396     prompt[0].hidden = 1;
397     prompt[0].reply = &pw0;
398     prompt_types[0] = KRB5_PROMPT_TYPE_NEW_PASSWORD;
399
400     pw1.data = pw1array;
401     pw1.data[0] = '\0';
402     pw1.length = sizeof(pw1array);
403     prompt[1].prompt = _("Enter it again");
404     prompt[1].hidden = 1;
405     prompt[1].reply = &pw1;
406     prompt_types[1] = KRB5_PROMPT_TYPE_NEW_PASSWORD_AGAIN;
407
408     strlcpy(banner, _("Password expired.  You must change it now."),
409             sizeof(banner));
410
411     for (tries = 3; tries; tries--) {
412         TRACE_GIC_PWD_CHANGEPW(context, tries);
413         pw0.length = sizeof(pw0array);
414         pw1.length = sizeof(pw1array);
415
416         /* PROMPTER_INVOCATION */
417         k5_set_prompt_types(context, prompt_types);
418         ret = (*prompter)(context, data, 0, banner,
419                           sizeof(prompt)/sizeof(prompt[0]), prompt);
420         k5_set_prompt_types(context, 0);
421         if (ret)
422             goto cleanup;
423
424         if (strcmp(pw0.data, pw1.data) != 0) {
425             ret = KRB5_LIBOS_BADPWDMATCH;
426             snprintf(banner, sizeof(banner),
427                      _("%s.  Please try again."), error_message(ret));
428         } else if (pw0.length == 0) {
429             ret = KRB5_CHPW_PWDNULL;
430             snprintf(banner, sizeof(banner),
431                      _("%s.  Please try again."), error_message(ret));
432         } else {
433             int result_code;
434             krb5_data code_string;
435             krb5_data result_string;
436
437             if ((ret = krb5_change_password(context, &chpw_creds, pw0array,
438                                             &result_code, &code_string,
439                                             &result_string)))
440                 goto cleanup;
441
442             /* the change succeeded.  go on */
443
444             if (result_code == 0) {
445                 free(code_string.data);
446                 free(result_string.data);
447                 break;
448             }
449
450             /* set this in case the retry loop falls through */
451
452             ret = KRB5_CHPW_FAIL;
453
454             if (result_code != KRB5_KPASSWD_SOFTERROR) {
455                 free(code_string.data);
456                 free(result_string.data);
457                 goto cleanup;
458             }
459
460             /* the error was soft, so try again */
461
462             if (krb5_chpw_message(context, &result_string, &message) != 0)
463                 message = NULL;
464
465             /* 100 is I happen to know that no code_string will be longer
466                than 100 chars */
467
468             if (message != NULL && strlen(message) > (sizeof(banner) - 100))
469                 message[sizeof(banner) - 100] = '\0';
470
471             snprintf(banner, sizeof(banner),
472                      _("%.*s%s%s.  Please try again.\n"),
473                      (int) code_string.length, code_string.data,
474                      message ? ": " : "", message ? message : "");
475
476             free(message);
477             free(code_string.data);
478             free(result_string.data);
479         }
480     }
481
482     if (ret)
483         goto cleanup;
484
485     /* the password change was successful.  Get an initial ticket
486        from the master.  this is the last try.  the return from this
487        is final.  */
488
489     TRACE_GIC_PWD_CHANGED(context);
490     gakpw.password = &pw0;
491     ret = k5_get_init_creds(context, creds, client, prompter, data,
492                             start_time, in_tkt_service, options,
493                             krb5_get_as_key_password, &gakpw, &use_master,
494                             &as_reply);
495     if (ret)
496         goto cleanup;
497
498 cleanup:
499     if (ret == 0)
500         warn_pw_expiry(context, options, prompter, data, in_tkt_service,
501                        as_reply);
502     free(chpw_opts);
503     zapfree(gakpw.storage.data, gakpw.storage.length);
504     memset(pw0array, 0, sizeof(pw0array));
505     memset(pw1array, 0, sizeof(pw1array));
506     krb5_free_cred_contents(context, &chpw_creds);
507     if (as_reply)
508         krb5_free_kdc_rep(context, as_reply);
509     k5_clear_error(&errsave);
510
511     return(ret);
512 }
513
514 /*
515   Rewrites get_in_tkt in terms of newer get_init_creds API.
516   Attempts to get an initial ticket for creds->client to use server
517   creds->server, (realm is taken from creds->client), with options
518   options, and using creds->times.starttime, creds->times.endtime,
519   creds->times.renew_till as from, till, and rtime.
520   creds->times.renew_till is ignored unless the RENEWABLE option is requested.
521
522   If addrs is non-NULL, it is used for the addresses requested.  If it is
523   null, the system standard addresses are used.
524
525   If password is non-NULL, it is converted using the cryptosystem entry
526   point for a string conversion routine, seeded with the client's name.
527   If password is passed as NULL, the password is read from the terminal,
528   and then converted into a key.
529
530   A succesful call will place the ticket in the credentials cache ccache.
531
532   returns system errors, encryption errors
533 */
534 krb5_error_code KRB5_CALLCONV
535 krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options,
536                               krb5_address *const *addrs, krb5_enctype *ktypes,
537                               krb5_preauthtype *pre_auth_types,
538                               const char *password, krb5_ccache ccache,
539                               krb5_creds *creds, krb5_kdc_rep **ret_as_reply)
540 {
541     krb5_error_code retval;
542     struct gak_password gakpw;
543     krb5_data pw;
544     char * server;
545     krb5_principal server_princ, client_princ;
546     int use_master = 0;
547     krb5_get_init_creds_opt *opts = NULL;
548
549     memset(&gakpw, 0, sizeof(gakpw));
550     if (password != NULL) {
551         pw = string2data((char *)password);
552         gakpw.password = &pw;
553     }
554     retval = k5_populate_gic_opt(context, &opts, options, addrs, ktypes,
555                                  pre_auth_types, creds);
556     if (retval)
557         return (retval);
558     retval = krb5_unparse_name( context, creds->server, &server);
559     if (retval) {
560         krb5_get_init_creds_opt_free(context, opts);
561         return (retval);
562     }
563     server_princ = creds->server;
564     client_princ = creds->client;
565     retval = k5_get_init_creds(context, creds, creds->client,
566                                krb5_prompter_posix, NULL, 0, server, opts,
567                                krb5_get_as_key_password, &gakpw, &use_master,
568                                ret_as_reply);
569     krb5_free_unparsed_name( context, server);
570     krb5_get_init_creds_opt_free(context, opts);
571     zapfree(gakpw.storage.data, gakpw.storage.length);
572     if (retval) {
573         return (retval);
574     }
575     krb5_free_principal( context, creds->server);
576     krb5_free_principal( context, creds->client);
577     creds->client = client_princ;
578     creds->server = server_princ;
579     /* store it in the ccache! */
580     if (ccache)
581         if ((retval = krb5_cc_store_cred(context, ccache, creds)))
582             return (retval);
583     return retval;
584 }