2000-04-28 Alexandra Ellwood <lxs@mit.edu>
[krb5.git] / src / lib / krb5 / krb / gic_pwd.c
1 #include "k5-int.h"
2 #include "com_err.h"
3
4 static krb5_error_code
5 krb5_get_as_key_password(context, client, etype, prompter, prompter_data,
6                          salt, as_key, gak_data)
7      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_keyblock *as_key;
14      void *gak_data;
15 {
16     krb5_data *password;
17     krb5_error_code ret;
18     krb5_data defsalt;
19     char *clientstr;
20     char promptstr[1024];
21     krb5_prompt prompt;
22     krb5_prompt_type prompt_type;
23
24     password = (krb5_data *) gak_data;
25
26     /* If there's already a key of the correct etype, we're done.
27        If the etype is wrong, free the existing key, and make
28        a new one.
29
30        XXX This was the old behavior, and was wrong in hw preauth
31        cases.  Is this new behavior -- always asking -- correct in all
32        cases?  */
33
34     if (as_key->length) {
35         if (as_key->enctype != etype) {
36             krb5_free_keyblock_contents (context, as_key);
37             as_key->length = 0;
38         }
39     }
40
41     if (password->data[0] == '\0') {
42         if (prompter == NULL)
43             return(EIO);
44
45         if ((ret = krb5_unparse_name(context, client, &clientstr)))
46             return(ret);
47
48         strcpy(promptstr, "Password for ");
49         strncat(promptstr, clientstr, sizeof(promptstr)-strlen(promptstr)-1);
50         promptstr[sizeof(promptstr)-1] = '\0';
51
52         free(clientstr);
53
54         prompt.prompt = promptstr;
55         prompt.hidden = 1;
56         prompt.reply = password;
57         prompt_type = KRB5_PROMPT_TYPE_PASSWORD;
58
59         /* PROMPTER_INVOCATION */
60         krb5int_set_prompt_types(context, &prompt_type);
61         if (ret = (((*prompter)(context, prompter_data, NULL, NULL,
62                                 1, &prompt)))) {
63             krb5int_set_prompt_types(context, 0);
64             return(ret);
65         }
66         krb5int_set_prompt_types(context, 0);
67     }
68
69     if ((salt->length == -1) && (salt->data == NULL)) {
70         if ((ret = krb5_principal2salt(context, client, &defsalt)))
71             return(ret);
72
73         salt = &defsalt;
74     } else {
75         defsalt.length = 0;
76     }
77
78     ret = krb5_c_string_to_key(context, etype, password, salt, as_key);
79
80     if (defsalt.length)
81         krb5_xfree(defsalt.data);
82
83     return(ret);
84 }
85
86 KRB5_DLLIMP krb5_error_code KRB5_CALLCONV
87 krb5_get_init_creds_password(context, creds, client, password, prompter, data,
88                              start_time, in_tkt_service, options)
89      krb5_context context;
90      krb5_creds *creds;
91      krb5_principal client;
92      char *password;
93      krb5_prompter_fct prompter;
94      void *data;
95      krb5_deltat start_time;
96      char *in_tkt_service;
97      krb5_get_init_creds_opt *options;
98 {
99    krb5_error_code ret, ret2;
100    int use_master;
101    krb5_kdc_rep *as_reply;
102    int tries;
103    krb5_creds chpw_creds;
104    krb5_get_init_creds_opt chpw_opts;
105    krb5_data pw0, pw1;
106    char banner[1024], pw0array[1024], pw1array[1024];
107    krb5_prompt prompt[2];
108    krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
109
110    use_master = 0;
111    as_reply = NULL;
112    memset(&chpw_creds, 0, sizeof(chpw_creds));
113
114    pw0.data = pw0array;
115
116    if (password) {
117       if ((pw0.length = strlen(password)) > sizeof(pw0array)) {
118          ret = EINVAL;
119          goto cleanup;
120       }
121       strcpy(pw0.data, password);
122    } else {
123       pw0.data[0] = '\0';
124       pw0.length = sizeof(pw0array);
125    }
126
127    pw1.data = pw1array;
128    pw1.data[0] = '\0';
129    pw1.length = sizeof(pw1array);
130
131    /* first try: get the requested tkt from any kdc */
132
133    ret = krb5_get_init_creds(context, creds, client, prompter, data,
134                              start_time, in_tkt_service, options,
135                              krb5_get_as_key_password, (void *) &pw0,
136                              use_master, &as_reply);
137
138    /* check for success */
139
140    if (ret == 0)
141       goto cleanup;
142
143    /* If all the kdc's are unavailable, or if the error was due to a
144       user interrupt, fail */
145
146    if ((ret == KRB5_KDC_UNREACH) ||
147        (ret == KRB5_LIBOS_PWDINTR) ||
148            (ret == KRB5_REALM_CANT_RESOLVE))
149       goto cleanup;
150
151    /* if the reply did not come from the master kdc, try again with
152       the master kdc */
153
154    if (!use_master) {
155       use_master = 1;
156
157       ret2 = krb5_get_init_creds(context, creds, client, prompter, data,
158                                  start_time, in_tkt_service, options,
159                                  krb5_get_as_key_password, (void *) &pw0,
160                                  use_master, &as_reply);
161       
162       if (ret2 == 0) {
163          ret = 0;
164          goto cleanup;
165       }
166
167       /* if the master is unreachable, return the error from the
168          slave we were able to contact */
169
170       if ((ret2 == KRB5_KDC_UNREACH) ||
171                   (ret2 == KRB5_REALM_CANT_RESOLVE))
172          goto cleanup;
173
174       ret = ret2;
175    }
176
177 #ifdef USE_LOGIN_LIBRARY
178         if (ret == KRB5KDC_ERR_KEY_EXP)
179                 goto cleanup;   /* Login library will deal appropriately with this error */
180 #endif
181
182    /* at this point, we have an error from the master.  if the error
183       is not password expired, or if it is but there's no prompter,
184       return this error */
185
186    if ((ret != KRB5KDC_ERR_KEY_EXP) ||
187        (prompter == NULL))
188       goto cleanup;
189
190    /* ok, we have an expired password.  Give the user a few chances
191       to change it */
192
193    /* use a minimal set of options */
194
195    krb5_get_init_creds_opt_init(&chpw_opts);
196    krb5_get_init_creds_opt_set_tkt_life(&chpw_opts, 5*60);
197    krb5_get_init_creds_opt_set_renew_life(&chpw_opts, 0);
198    krb5_get_init_creds_opt_set_forwardable(&chpw_opts, 0);
199    krb5_get_init_creds_opt_set_proxiable(&chpw_opts, 0);
200
201    if ((ret = krb5_get_init_creds(context, &chpw_creds, client,
202                                   prompter, data,
203                                   start_time, "kadmin/changepw", &chpw_opts,
204                                   krb5_get_as_key_password, (void *) &pw0,
205                                   use_master, NULL)))
206       goto cleanup;
207
208    prompt[0].prompt = "Enter new password";
209    prompt[0].hidden = 1;
210    prompt[0].reply = &pw0;
211    prompt_types[0] = KRB5_PROMPT_TYPE_NEW_PASSWORD;
212
213    prompt[1].prompt = "Enter it again";
214    prompt[1].hidden = 1;
215    prompt[1].reply = &pw1;
216    prompt_types[1] = KRB5_PROMPT_TYPE_NEW_PASSWORD_AGAIN;
217
218    strcpy(banner, "Password expired.  You must change it now.");
219
220    for (tries = 3; tries; tries--) {
221       pw0.length = sizeof(pw0array);
222       pw1.length = sizeof(pw1array);
223
224       /* PROMPTER_INVOCATION */
225       krb5int_set_prompt_types(context, prompt_types);
226       if (ret = ((*prompter)(context, data, 0, banner,
227                              sizeof(prompt)/sizeof(prompt[0]), prompt)))
228          goto cleanup;
229       krb5int_set_prompt_types(context, 0);
230
231
232       if (strcmp(pw0.data, pw1.data) != 0) {
233          ret = KRB5_LIBOS_BADPWDMATCH;
234          sprintf(banner, "%s.  Please try again.", error_message(ret));
235       } else if (pw0.length == 0) {
236          ret = KRB5_CHPW_PWDNULL;
237          sprintf(banner, "%s.  Please try again.", error_message(ret));
238       } else {
239          int result_code;
240          krb5_data code_string;
241          krb5_data result_string;
242
243          if ((ret = krb5_change_password(context, &chpw_creds, pw0array,
244                                          &result_code, &code_string,
245                                          &result_string)))
246             goto cleanup;
247
248          /* the change succeeded.  go on */
249
250          if (result_code == 0) {
251             krb5_xfree(result_string.data);
252             break;
253          }
254
255          /* set this in case the retry loop falls through */
256
257          ret = KRB5_CHPW_FAIL;
258
259          if (result_code != KRB5_KPASSWD_SOFTERROR) {
260             krb5_xfree(result_string.data);
261             goto cleanup;
262          }
263
264          /* the error was soft, so try again */
265
266          /* 100 is I happen to know that no code_string will be longer
267             than 100 chars */
268
269          if (result_string.length > (sizeof(banner)-100))
270             result_string.length = sizeof(banner)-100;
271
272          sprintf(banner, "%.*s%s%.*s.  Please try again.\n",
273                  code_string.length, code_string.data,
274                  result_string.length?": ":"",
275                  result_string.length, result_string.data);
276
277          krb5_xfree(code_string.data);
278          krb5_xfree(result_string.data);
279       }
280    }
281
282    if (ret)
283       goto cleanup;
284
285    /* the password change was successful.  Get an initial ticket
286       from the master.  this is the last try.  the return from this
287       is final.  */
288
289    ret = krb5_get_init_creds(context, creds, client, prompter, data,
290                              start_time, in_tkt_service, options,
291                              krb5_get_as_key_password, (void *) &pw0,
292                              use_master, &as_reply);
293
294 cleanup:
295    krb5int_set_prompt_types(context, 0);
296    /* if getting the password was successful, then check to see if the
297       password is about to expire, and warn if so */
298
299    if (ret == 0) {
300       krb5_timestamp now;
301       int hours;
302
303       /* XXX 7 days should be configurable.  This is all pretty ad hoc,
304          and could probably be improved if I was willing to screw around
305          with timezones, etc. */
306
307       if (prompter &&
308           (in_tkt_service &&
309            (strcmp(in_tkt_service, "kadmin/changepw") != 0)) &&
310           ((ret = krb5_timeofday(context, &now)) == 0) &&
311           as_reply->enc_part2->key_exp &&
312           ((hours = ((as_reply->enc_part2->key_exp-now)/(60*60))) <= 7*24) &&
313           (hours >= 0)) {
314          if (hours < 1)
315             sprintf(banner,
316                     "Warning: Your password will expire in less than one hour.");
317          else if (hours <= 48)
318             sprintf(banner, "Warning: Your password will expire in %d hour%s.",
319                     hours, (hours == 1)?"":"s");
320          else
321             sprintf(banner, "Warning: Your password will expire in %d days.",
322                     hours/24);
323
324          /* ignore an error here */
325          /* PROMPTER_INVOCATION */
326          (*prompter)(context, data, 0, banner, 0, 0);
327       }
328    }
329
330    memset(pw0array, 0, sizeof(pw0array));
331    memset(pw1array, 0, sizeof(pw1array));
332    krb5_free_cred_contents(context, &chpw_creds);
333    if (as_reply)
334       krb5_free_kdc_rep(context, as_reply);
335
336    return(ret);
337 }