Add API to interpret changepw result strings
[krb5.git] / src / lib / krb5 / krb / chpw.c
1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3 ** set password functions added by Paul W. Nelson, Thursby Software Systems, Inc.
4 */
5 #include <string.h>
6
7 #include "k5-int.h"
8 #include "k5-unicode.h"
9 #include "int-proto.h"
10 #include "auth_con.h"
11
12
13 krb5_error_code
14 krb5int_mk_chpw_req(krb5_context context,
15                     krb5_auth_context auth_context,
16                     krb5_data *ap_req,
17                     char *passwd,
18                     krb5_data *packet)
19 {
20     krb5_error_code ret = 0;
21     krb5_data clearpw;
22     krb5_data cipherpw;
23     krb5_replay_data replay;
24     char *ptr;
25
26     cipherpw.data = NULL;
27
28     if ((ret = krb5_auth_con_setflags(context, auth_context,
29                                       KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
30         goto cleanup;
31
32     clearpw.length = strlen(passwd);
33     clearpw.data = passwd;
34
35     if ((ret = krb5_mk_priv(context, auth_context,
36                             &clearpw, &cipherpw, &replay)))
37         goto cleanup;
38
39     packet->length = 6 + ap_req->length + cipherpw.length;
40     packet->data = (char *) malloc(packet->length);
41     if (packet->data == NULL) {
42         ret = ENOMEM;
43         goto cleanup;
44     }
45     ptr = packet->data;
46
47     /* length */
48
49     store_16_be(packet->length, ptr);
50     ptr += 2;
51
52     /* version == 0x0001 big-endian */
53
54     *ptr++ = 0;
55     *ptr++ = 1;
56
57     /* ap_req length, big-endian */
58
59     store_16_be(ap_req->length, ptr);
60     ptr += 2;
61
62     /* ap-req data */
63
64     memcpy(ptr, ap_req->data, ap_req->length);
65     ptr += ap_req->length;
66
67     /* krb-priv of password */
68
69     memcpy(ptr, cipherpw.data, cipherpw.length);
70
71 cleanup:
72     if (cipherpw.data != NULL)  /* allocated by krb5_mk_priv */
73         free(cipherpw.data);
74
75     return(ret);
76 }
77
78 /* Decode error_packet as a KRB-ERROR message and retrieve its e-data into
79  * *edata_out. */
80 static krb5_error_code
81 get_error_edata(krb5_context context, const krb5_data *error_packet,
82                 krb5_data **edata_out)
83 {
84     krb5_error_code ret;
85     krb5_error *krberror = NULL;
86
87     *edata_out = NULL;
88
89     ret = krb5_rd_error(context, error_packet, &krberror);
90     if (ret)
91         return ret;
92
93     if (krberror->e_data.data == NULL) {
94         /* Return a krb5 error code based on the error number. */
95         ret = ERROR_TABLE_BASE_krb5 + (krb5_error_code)krberror->error;
96         goto cleanup;
97     }
98
99     ret = krb5_copy_data(context, &krberror->e_data, edata_out);
100
101 cleanup:
102     krb5_free_error(context, krberror);
103     return ret;
104 }
105
106 /* Decode a reply to produce the clear-text output. */
107 static krb5_error_code
108 get_clear_result(krb5_context context, krb5_auth_context auth_context,
109                  const krb5_data *packet, krb5_data **clear_out,
110                  krb5_boolean *is_error_out)
111 {
112     krb5_error_code ret;
113     char *ptr, *end = packet->data + packet->length;
114     unsigned int plen, vno, aplen;
115     krb5_data ap_rep, cipher, error;
116     krb5_ap_rep_enc_part *ap_rep_enc;
117     krb5_replay_data replay;
118     krb5_key send_subkey = NULL;
119     krb5_data clear = empty_data();
120
121     *clear_out = NULL;
122     *is_error_out = FALSE;
123
124     /* Check for an unframed KRB-ERROR (expected for RFC 3244 requests; also
125      * received from MS AD for version 1 requests). */
126     if (krb5_is_krb_error(packet)) {
127         *is_error_out = TRUE;
128         return get_error_edata(context, packet, clear_out);
129     }
130
131     if (packet->length < 6)
132         return KRB5KRB_AP_ERR_MODIFIED;
133
134     /* Decode and verify the length. */
135     ptr = packet->data;
136     plen = (*ptr++ & 0xff);
137     plen = (plen << 8) | (*ptr++ & 0xff);
138     if (plen != packet->length)
139         return KRB5KRB_AP_ERR_MODIFIED;
140
141     /* Decode and verify the version number. */
142     vno = (*ptr++ & 0xff);
143     vno = (vno << 8) | (*ptr++ & 0xff);
144     if (vno != 1 && vno != 0xff80)
145         return KRB5KDC_ERR_BAD_PVNO;
146
147     /* Decode and check the AP-REP length. */
148     aplen = (*ptr++ & 0xff);
149     aplen = (aplen << 8) | (*ptr++ & 0xff);
150     if (aplen > end - ptr)
151         return KRB5KRB_AP_ERR_MODIFIED;
152
153     /* A zero-length AP-REQ indicates a framed KRB-ERROR response.  (Expected
154      * for protocol version 1; specified but unusual for RFC 3244 requests.) */
155     if (aplen == 0) {
156         *is_error_out = TRUE;
157         error = make_data(ptr, end - ptr);
158         return get_error_edata(context, &error, clear_out);
159     }
160
161     /* We have an AP-REP.  Save send_subkey to later smash recv_subkey. */
162     ret = krb5_auth_con_getsendsubkey_k(context, auth_context, &send_subkey);
163     if (ret)
164         return ret;
165
166     /* Verify the AP-REP. */
167     ap_rep = make_data(ptr, aplen);
168     ptr += ap_rep.length;
169     ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc);
170     if (ret)
171         goto cleanup;
172     krb5_free_ap_rep_enc_part(context, ap_rep_enc);
173
174     /* Smash recv_subkey to be send_subkey, per spec. */
175     ret = krb5_auth_con_setrecvsubkey_k(context, auth_context, send_subkey);
176     if (ret)
177         goto cleanup;
178
179     /* Extract and decrypt the result. */
180     cipher = make_data(ptr, end - ptr);
181     ret = krb5_rd_priv(context, auth_context, &cipher, &clear, &replay);
182     if (ret)
183         goto cleanup;
184
185     ret = krb5_copy_data(context, &clear, clear_out);
186     if (ret)
187         goto cleanup;
188     *is_error_out = FALSE;
189
190 cleanup:
191     krb5_k_free_key(context, send_subkey);
192     krb5_free_data_contents(context, &clear);
193     return ret;
194 }
195
196 krb5_error_code
197 krb5int_rd_chpw_rep(krb5_context context, krb5_auth_context auth_context,
198                     krb5_data *packet, int *result_code_out,
199                     krb5_data *result_data_out)
200 {
201     krb5_error_code ret;
202     krb5_data result_data, *clear = NULL;
203     krb5_boolean is_error;
204     char *ptr;
205     int result_code;
206
207     *result_code_out = 0;
208     *result_data_out = empty_data();
209
210     ret = get_clear_result(context, auth_context, packet, &clear, &is_error);
211     if (ret)
212         return ret;
213
214     if (clear->length < 2) {
215         ret = KRB5KRB_AP_ERR_MODIFIED;
216         goto cleanup;
217     }
218
219     /* Decode and check the result code. */
220     ptr = clear->data;
221     result_code = (*ptr++ & 0xff);
222     result_code = (result_code << 8) | (*ptr++ & 0xff);
223     if (result_code < KRB5_KPASSWD_SUCCESS ||
224         result_code > KRB5_KPASSWD_INITIAL_FLAG_NEEDED) {
225         ret = KRB5KRB_AP_ERR_MODIFIED;
226         goto cleanup;
227     }
228
229     /* Successful replies must not come from errors. */
230     if (is_error && result_code == KRB5_KPASSWD_SUCCESS) {
231         ret = KRB5KRB_AP_ERR_MODIFIED;
232         goto cleanup;
233     }
234
235     result_data = make_data(ptr, clear->data + clear->length - ptr);
236     ret = krb5int_copy_data_contents(context, &result_data, result_data_out);
237     if (ret)
238         goto cleanup;
239     *result_code_out = result_code;
240
241 cleanup:
242     krb5_free_data(context, clear);
243     return ret;
244 }
245
246 krb5_error_code KRB5_CALLCONV
247 krb5_chpw_result_code_string(krb5_context context, int result_code,
248                              char **code_string)
249 {
250     switch (result_code) {
251     case KRB5_KPASSWD_MALFORMED:
252         *code_string = _("Malformed request error");
253         break;
254     case KRB5_KPASSWD_HARDERROR:
255         *code_string = _("Server error");
256         break;
257     case KRB5_KPASSWD_AUTHERROR:
258         *code_string = _("Authentication error");
259         break;
260     case KRB5_KPASSWD_SOFTERROR:
261         *code_string = _("Password change rejected");
262         break;
263     case KRB5_KPASSWD_ACCESSDENIED:
264         *code_string = _("Access denied");
265         break;
266     case KRB5_KPASSWD_BAD_VERSION:
267         *code_string = _("Wrong protocol version");
268         break;
269     case KRB5_KPASSWD_INITIAL_FLAG_NEEDED:
270         *code_string = _("Initial password required");
271         break;
272     default:
273         *code_string = _("Password change failed");
274         break;
275     }
276
277     return 0;
278 }
279
280 krb5_error_code
281 krb5int_mk_setpw_req(krb5_context context,
282                      krb5_auth_context auth_context,
283                      krb5_data *ap_req,
284                      krb5_principal targprinc,
285                      char *passwd,
286                      krb5_data *packet)
287 {
288     krb5_error_code ret;
289     krb5_data   cipherpw;
290     krb5_data   *encoded_setpw;
291     struct krb5_setpw_req req;
292
293     char *ptr;
294
295     cipherpw.data = NULL;
296     cipherpw.length = 0;
297
298     if ((ret = krb5_auth_con_setflags(context, auth_context,
299                                       KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
300         return(ret);
301
302     req.target = targprinc;
303     req.password.data = passwd;
304     req.password.length = strlen(passwd);
305     ret = encode_krb5_setpw_req(&req, &encoded_setpw);
306     if (ret) {
307         return ret;
308     }
309
310     if ((ret = krb5_mk_priv(context, auth_context, encoded_setpw, &cipherpw, NULL)) != 0) {
311         krb5_free_data(context, encoded_setpw);
312         return(ret);
313     }
314     krb5_free_data(context, encoded_setpw);
315
316
317     packet->length = 6 + ap_req->length + cipherpw.length;
318     packet->data = (char *) malloc(packet->length);
319     if (packet->data  == NULL) {
320         ret = ENOMEM;
321         goto cleanup;
322     }
323     ptr = packet->data;
324     /*
325     ** build the packet -
326     */
327     /* put in the length */
328     store_16_be(packet->length, ptr);
329     ptr += 2;
330     /* put in the version */
331     *ptr++ = (char)0xff;
332     *ptr++ = (char)0x80;
333     /* the ap_req length is big endian */
334     store_16_be(ap_req->length, ptr);
335     ptr += 2;
336     /* put in the request data */
337     memcpy(ptr, ap_req->data, ap_req->length);
338     ptr += ap_req->length;
339     /*
340     ** put in the "private" password data -
341     */
342     memcpy(ptr, cipherpw.data, cipherpw.length);
343     ret = 0;
344 cleanup:
345     if (cipherpw.data)
346         krb5_free_data_contents(context, &cipherpw);
347     if ((ret != 0) && packet->data) {
348         free(packet->data);
349         packet->data = NULL;
350     }
351     return ret;
352 }
353
354 /*
355  * Active Directory policy information is communicated in the result string
356  * field as a packed 30-byte sequence, starting with two zero bytes (so that
357  * the string appears as zero-length when interpreted as UTF-8).  The bytes
358  * correspond to the fields in the following structure, with each field in
359  * big-endian byte order.
360  */
361 struct ad_policy_info {
362     uint16_t zero_bytes;
363     uint32_t min_length_password;
364     uint32_t password_history;
365     uint32_t password_properties; /* see defines below */
366     uint64_t expire;              /* in seconds * 10,000,000 */
367     uint64_t min_passwordage;     /* in seconds * 10,000,000 */
368 };
369
370 #define AD_POLICY_INFO_LENGTH      30
371 #define AD_POLICY_TIME_TO_DAYS     (86400ULL * 10000000ULL)
372
373 #define AD_POLICY_COMPLEX          0x00000001
374 #define AD_POLICY_NO_ANON_CHANGE   0x00000002
375 #define AD_POLICY_NO_CLEAR_CHANGE  0x00000004
376 #define AD_POLICY_LOCKOUT_ADMINS   0x00000008
377 #define AD_POLICY_STORE_CLEARTEXT  0x00000010
378 #define AD_POLICY_REFUSE_CHANGE    0x00000020
379
380 /* If buf already contains one or more sentences, add spaces to separate them
381  * from the next sentence. */
382 static void
383 add_spaces(struct k5buf *buf)
384 {
385     if (krb5int_buf_len(buf) > 0)
386         krb5int_buf_add(buf, "  ");
387 }
388
389 static krb5_error_code
390 decode_ad_policy_info(const krb5_data *data, char **msg_out)
391 {
392     struct ad_policy_info policy;
393     uint64_t password_days;
394     const char *p;
395     char *msg;
396     struct k5buf buf;
397
398     *msg_out = NULL;
399     if (data->length != AD_POLICY_INFO_LENGTH)
400         return 0;
401
402     p = data->data;
403     policy.zero_bytes = load_16_be(p);
404     p += 2;
405
406     /* first two bytes are zeros */
407     if (policy.zero_bytes != 0)
408         return 0;
409
410     /* Read in the rest of structure */
411     policy.min_length_password = load_32_be(p);
412     p += 4;
413     policy.password_history = load_32_be(p);
414     p += 4;
415     policy.password_properties = load_32_be(p);
416     p += 4;
417     policy.expire = load_64_be(p);
418     p += 8;
419     policy.min_passwordage = load_64_be(p);
420     p += 8;
421
422     /* Check that we processed exactly the expected number of bytes. */
423     assert(p == data->data + AD_POLICY_INFO_LENGTH);
424
425     krb5int_buf_init_dynamic(&buf);
426
427     /*
428      * Update src/tests/misc/test_chpw_message.c if changing these strings!
429      */
430
431     if (policy.password_properties & AD_POLICY_COMPLEX) {
432         krb5int_buf_add(&buf,
433                         _("The password must include numbers or symbols.  "
434                           "Don't include any part of your name in the "
435                           "password."));
436     }
437     if (policy.min_length_password > 0) {
438         add_spaces(&buf);
439         krb5int_buf_add_fmt(&buf,
440                             ngettext("The password must contain at least %d "
441                                      "character.",
442                                      "The password must contain at least %d "
443                                      "characters.",
444                                      policy.min_length_password),
445                             policy.min_length_password);
446     }
447     if (policy.password_history) {
448         add_spaces(&buf);
449         krb5int_buf_add_fmt(&buf,
450                             ngettext("The password must be different from the "
451                                      "previous password.",
452                                      "The password must be different from the "
453                                      "previous %d passwords.",
454                                      policy.password_history),
455                             policy.password_history);
456     }
457     if (policy.min_passwordage) {
458         password_days = policy.min_passwordage / AD_POLICY_TIME_TO_DAYS;
459         if (password_days == 0)
460             password_days = 1;
461         add_spaces(&buf);
462         krb5int_buf_add_fmt(&buf,
463                             ngettext("The password can only be changed once a "
464                                      "day.",
465                                      "The password can only be changed every "
466                                      "%d days.", (int)password_days),
467                             (int)password_days);
468     }
469
470     msg = krb5int_buf_data(&buf);
471     if (msg == NULL)
472         return ENOMEM;
473
474     if (*msg != '\0')
475         *msg_out = msg;
476     else
477         free(msg);
478     return 0;
479 }
480
481 krb5_error_code KRB5_CALLCONV
482 krb5_chpw_message(krb5_context context, const krb5_data *server_string,
483                   char **message_out)
484 {
485     krb5_error_code ret;
486     krb5_data *string;
487     char *msg;
488
489     *message_out = NULL;
490
491     /* If server_string contains an AD password policy, construct a message
492      * based on that. */
493     ret = decode_ad_policy_info(server_string, &msg);
494     if (ret == 0 && msg != NULL) {
495         *message_out = msg;
496         return 0;
497     }
498
499     /* If server_string contains a valid UTF-8 string, return that. */
500     if (server_string->length > 0 &&
501         memchr(server_string->data, 0, server_string->length) == NULL &&
502         krb5int_utf8_normalize(server_string, &string,
503                                KRB5_UTF8_APPROX) == 0) {
504         *message_out = string->data; /* already null terminated */
505         free(string);
506         return 0;
507     }
508
509     /* server_string appears invalid, so try to be helpful. */
510     msg = strdup(_("Try a more complex password, or contact your "
511                    "administrator."));
512     if (msg == NULL)
513         return ENOMEM;
514
515     *message_out = msg;
516     return 0;
517 }