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