Add API to interpret changepw result strings
[krb5.git] / src / lib / krb5 / krb / chpw.c
index 3f359ba71675a95ec2d2429591402fc2cecef859..beb77cb986f9f10a8879df776900d00d5254db66 100644 (file)
@@ -5,6 +5,8 @@
 #include <string.h>
 
 #include "k5-int.h"
+#include "k5-unicode.h"
+#include "int-proto.h"
 #include "auth_con.h"
 
 
@@ -73,166 +75,172 @@ cleanup:
     return(ret);
 }
 
-krb5_error_code
-krb5int_rd_chpw_rep(krb5_context context, krb5_auth_context auth_context,
-                    krb5_data *packet, int *result_code, krb5_data *result_data)
+/* Decode error_packet as a KRB-ERROR message and retrieve its e-data into
+ * *edata_out. */
+static krb5_error_code
+get_error_edata(krb5_context context, const krb5_data *error_packet,
+                krb5_data **edata_out)
 {
-    char *ptr;
-    unsigned int plen, vno;
-    krb5_data ap_rep;
-    krb5_ap_rep_enc_part *ap_rep_enc;
     krb5_error_code ret;
-    krb5_data cipherresult;
-    krb5_data clearresult;
     krb5_error *krberror = NULL;
-    krb5_replay_data replay;
-    krb5_keyblock *tmp;
-
-    if (packet->length < 4)
-        /* either this, or the server is printing bad messages,
-           or the caller passed in garbage */
-        return(KRB5KRB_AP_ERR_MODIFIED);
 
-    ptr = packet->data;
+    *edata_out = NULL;
 
-    /* verify length */
+    ret = krb5_rd_error(context, error_packet, &krberror);
+    if (ret)
+        return ret;
 
-    plen = (*ptr++ & 0xff);
-    plen = (plen<<8) | (*ptr++ & 0xff);
-
-    if (plen != packet->length) {
-        /*
-         * MS KDCs *may* send back a KRB_ERROR.  Although
-         * not 100% correct via RFC3244, it's something
-         * we can workaround here.
-         */
-        if (krb5_is_krb_error(packet)) {
-
-            if ((ret = krb5_rd_error(context, packet, &krberror)))
-                return(ret);
-
-            if (krberror->e_data.data  == NULL)
-                ret = ERROR_TABLE_BASE_krb5 + (krb5_error_code) krberror->error;
-            else
-                ret = KRB5KRB_AP_ERR_MODIFIED;
-            krb5_free_error(context, krberror);
-            return(ret);
-        } else {
-            return(KRB5KRB_AP_ERR_MODIFIED);
-        }
+    if (krberror->e_data.data == NULL) {
+        /* Return a krb5 error code based on the error number. */
+        ret = ERROR_TABLE_BASE_krb5 + (krb5_error_code)krberror->error;
+        goto cleanup;
     }
 
+    ret = krb5_copy_data(context, &krberror->e_data, edata_out);
 
-    /* verify version number */
-
-    vno = (*ptr++ & 0xff);
-    vno = (vno<<8) | (*ptr++ & 0xff);
+cleanup:
+    krb5_free_error(context, krberror);
+    return ret;
+}
 
-    if (vno != 1)
-        return(KRB5KDC_ERR_BAD_PVNO);
+/* Decode a reply to produce the clear-text output. */
+static krb5_error_code
+get_clear_result(krb5_context context, krb5_auth_context auth_context,
+                 const krb5_data *packet, krb5_data **clear_out,
+                 krb5_boolean *is_error_out)
+{
+    krb5_error_code ret;
+    char *ptr, *end = packet->data + packet->length;
+    unsigned int plen, vno, aplen;
+    krb5_data ap_rep, cipher, error;
+    krb5_ap_rep_enc_part *ap_rep_enc;
+    krb5_replay_data replay;
+    krb5_key send_subkey = NULL;
+    krb5_data clear = empty_data();
 
-    /* read, check ap-rep length */
+    *clear_out = NULL;
+    *is_error_out = FALSE;
 
-    ap_rep.length = (*ptr++ & 0xff);
-    ap_rep.length = (ap_rep.length<<8) | (*ptr++ & 0xff);
+    /* Check for an unframed KRB-ERROR (expected for RFC 3244 requests; also
+     * received from MS AD for version 1 requests). */
+    if (krb5_is_krb_error(packet)) {
+        *is_error_out = TRUE;
+        return get_error_edata(context, packet, clear_out);
+    }
 
-    if (ptr + ap_rep.length >= packet->data + packet->length)
-        return(KRB5KRB_AP_ERR_MODIFIED);
+    if (packet->length < 6)
+        return KRB5KRB_AP_ERR_MODIFIED;
 
-    if (ap_rep.length) {
-        /* verify ap_rep */
-        ap_rep.data = ptr;
-        ptr += ap_rep.length;
+    /* Decode and verify the length. */
+    ptr = packet->data;
+    plen = (*ptr++ & 0xff);
+    plen = (plen << 8) | (*ptr++ & 0xff);
+    if (plen != packet->length)
+        return KRB5KRB_AP_ERR_MODIFIED;
 
-        /*
-         * Save send_subkey to later smash recv_subkey.
-         */
-        ret = krb5_auth_con_getsendsubkey(context, auth_context, &tmp);
-        if (ret)
-            return ret;
+    /* Decode and verify the version number. */
+    vno = (*ptr++ & 0xff);
+    vno = (vno << 8) | (*ptr++ & 0xff);
+    if (vno != 1 && vno != 0xff80)
+        return KRB5KDC_ERR_BAD_PVNO;
+
+    /* Decode and check the AP-REP length. */
+    aplen = (*ptr++ & 0xff);
+    aplen = (aplen << 8) | (*ptr++ & 0xff);
+    if (aplen > end - ptr)
+        return KRB5KRB_AP_ERR_MODIFIED;
+
+    /* A zero-length AP-REQ indicates a framed KRB-ERROR response.  (Expected
+     * for protocol version 1; specified but unusual for RFC 3244 requests.) */
+    if (aplen == 0) {
+        *is_error_out = TRUE;
+        error = make_data(ptr, end - ptr);
+        return get_error_edata(context, &error, clear_out);
+    }
 
-        ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc);
-        if (ret) {
-            krb5_free_keyblock(context, tmp);
-            return(ret);
-        }
+    /* We have an AP-REP.  Save send_subkey to later smash recv_subkey. */
+    ret = krb5_auth_con_getsendsubkey_k(context, auth_context, &send_subkey);
+    if (ret)
+        return ret;
 
-        krb5_free_ap_rep_enc_part(context, ap_rep_enc);
+    /* Verify the AP-REP. */
+    ap_rep = make_data(ptr, aplen);
+    ptr += ap_rep.length;
+    ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc);
+    if (ret)
+        goto cleanup;
+    krb5_free_ap_rep_enc_part(context, ap_rep_enc);
 
-        /* extract and decrypt the result */
+    /* Smash recv_subkey to be send_subkey, per spec. */
+    ret = krb5_auth_con_setrecvsubkey_k(context, auth_context, send_subkey);
+    if (ret)
+        goto cleanup;
 
-        cipherresult.data = ptr;
-        cipherresult.length = (packet->data + packet->length) - ptr;
+    /* Extract and decrypt the result. */
+    cipher = make_data(ptr, end - ptr);
+    ret = krb5_rd_priv(context, auth_context, &cipher, &clear, &replay);
+    if (ret)
+        goto cleanup;
 
-        /*
-         * Smash recv_subkey to be send_subkey, per spec.
-         */
-        ret = krb5_auth_con_setrecvsubkey(context, auth_context, tmp);
-        krb5_free_keyblock(context, tmp);
-        if (ret)
-            return ret;
+    ret = krb5_copy_data(context, &clear, clear_out);
+    if (ret)
+        goto cleanup;
+    *is_error_out = FALSE;
 
-        ret = krb5_rd_priv(context, auth_context, &cipherresult, &clearresult,
-                           &replay);
+cleanup:
+    krb5_k_free_key(context, send_subkey);
+    krb5_free_data_contents(context, &clear);
+    return ret;
+}
 
-        if (ret)
-            return(ret);
-    } else {
-        cipherresult.data = ptr;
-        cipherresult.length = (packet->data + packet->length) - ptr;
+krb5_error_code
+krb5int_rd_chpw_rep(krb5_context context, krb5_auth_context auth_context,
+                    krb5_data *packet, int *result_code_out,
+                    krb5_data *result_data_out)
+{
+    krb5_error_code ret;
+    krb5_data result_data, *clear = NULL;
+    krb5_boolean is_error;
+    char *ptr;
+    int result_code;
 
-        if ((ret = krb5_rd_error(context, &cipherresult, &krberror)))
-            return(ret);
+    *result_code_out = 0;
+    *result_data_out = empty_data();
 
-        clearresult = krberror->e_data;
-    }
+    ret = get_clear_result(context, auth_context, packet, &clear, &is_error);
+    if (ret)
+        return ret;
 
-    if (clearresult.length < 2) {
+    if (clear->length < 2) {
         ret = KRB5KRB_AP_ERR_MODIFIED;
         goto cleanup;
     }
 
-    ptr = clearresult.data;
-
-    *result_code = (*ptr++ & 0xff);
-    *result_code = (*result_code<<8) | (*ptr++ & 0xff);
-
-    if ((*result_code < KRB5_KPASSWD_SUCCESS) ||
-        (*result_code > KRB5_KPASSWD_INITIAL_FLAG_NEEDED)) {
+    /* Decode and check the result code. */
+    ptr = clear->data;
+    result_code = (*ptr++ & 0xff);
+    result_code = (result_code << 8) | (*ptr++ & 0xff);
+    if (result_code < KRB5_KPASSWD_SUCCESS ||
+        result_code > KRB5_KPASSWD_INITIAL_FLAG_NEEDED) {
         ret = KRB5KRB_AP_ERR_MODIFIED;
         goto cleanup;
     }
 
-    /* all success replies should be authenticated/encrypted */
-
-    if ((ap_rep.length == 0) && (*result_code == KRB5_KPASSWD_SUCCESS)) {
+    /* Successful replies must not come from errors. */
+    if (is_error && result_code == KRB5_KPASSWD_SUCCESS) {
         ret = KRB5KRB_AP_ERR_MODIFIED;
         goto cleanup;
     }
 
-    result_data->length = (clearresult.data + clearresult.length) - ptr;
-
-    if (result_data->length) {
-        result_data->data = (char *) malloc(result_data->length);
-        if (result_data->data == NULL) {
-            ret = ENOMEM;
-            goto cleanup;
-        }
-        memcpy(result_data->data, ptr, result_data->length);
-    } else {
-        result_data->data = NULL;
-    }
-
-    ret = 0;
+    result_data = make_data(ptr, clear->data + clear->length - ptr);
+    ret = krb5int_copy_data_contents(context, &result_data, result_data_out);
+    if (ret)
+        goto cleanup;
+    *result_code_out = result_code;
 
 cleanup:
-    if (ap_rep.length) {
-        free(clearresult.data);
-    } else {
-        krb5_free_error(context, krberror);
-    }
-
-    return(ret);
+    krb5_free_data(context, clear);
+    return ret;
 }
 
 krb5_error_code KRB5_CALLCONV
@@ -241,23 +249,32 @@ krb5_chpw_result_code_string(krb5_context context, int result_code,
 {
     switch (result_code) {
     case KRB5_KPASSWD_MALFORMED:
-        *code_string = "Malformed request error";
+        *code_string = _("Malformed request error");
         break;
     case KRB5_KPASSWD_HARDERROR:
-        *code_string = "Server error";
+        *code_string = _("Server error");
         break;
     case KRB5_KPASSWD_AUTHERROR:
-        *code_string = "Authentication error";
+        *code_string = _("Authentication error");
         break;
     case KRB5_KPASSWD_SOFTERROR:
-        *code_string = "Password change rejected";
+        *code_string = _("Password change rejected");
+        break;
+    case KRB5_KPASSWD_ACCESSDENIED:
+        *code_string = _("Access denied");
+        break;
+    case KRB5_KPASSWD_BAD_VERSION:
+        *code_string = _("Wrong protocol version");
+        break;
+    case KRB5_KPASSWD_INITIAL_FLAG_NEEDED:
+        *code_string = _("Initial password required");
         break;
     default:
-        *code_string = "Password change failed";
+        *code_string = _("Password change failed");
         break;
     }
 
-    return(0);
+    return 0;
 }
 
 krb5_error_code
@@ -334,208 +351,167 @@ cleanup:
     return ret;
 }
 
-krb5_error_code
-krb5int_rd_setpw_rep(krb5_context context, krb5_auth_context auth_context,
-                     krb5_data *packet,
-                     int *result_code, krb5_data *result_data)
+/*
+ * Active Directory policy information is communicated in the result string
+ * field as a packed 30-byte sequence, starting with two zero bytes (so that
+ * the string appears as zero-length when interpreted as UTF-8).  The bytes
+ * correspond to the fields in the following structure, with each field in
+ * big-endian byte order.
+ */
+struct ad_policy_info {
+    uint16_t zero_bytes;
+    uint32_t min_length_password;
+    uint32_t password_history;
+    uint32_t password_properties; /* see defines below */
+    uint64_t expire;              /* in seconds * 10,000,000 */
+    uint64_t min_passwordage;     /* in seconds * 10,000,000 */
+};
+
+#define AD_POLICY_INFO_LENGTH      30
+#define AD_POLICY_TIME_TO_DAYS     (86400ULL * 10000000ULL)
+
+#define AD_POLICY_COMPLEX          0x00000001
+#define AD_POLICY_NO_ANON_CHANGE   0x00000002
+#define AD_POLICY_NO_CLEAR_CHANGE  0x00000004
+#define AD_POLICY_LOCKOUT_ADMINS   0x00000008
+#define AD_POLICY_STORE_CLEARTEXT  0x00000010
+#define AD_POLICY_REFUSE_CHANGE    0x00000020
+
+/* If buf already contains one or more sentences, add spaces to separate them
+ * from the next sentence. */
+static void
+add_spaces(struct k5buf *buf)
 {
-    char *ptr;
-    unsigned int message_length, version_number;
-    krb5_data ap_rep;
-    krb5_ap_rep_enc_part *ap_rep_enc;
-    krb5_error_code ret;
-    krb5_data cipherresult;
-    krb5_data clearresult;
-    krb5_keyblock *tmpkey;
-    /*
-    ** validate the packet length -
-    */
-    if (packet->length < 4)
-        return(KRB5KRB_AP_ERR_MODIFIED);
-
-    ptr = packet->data;
+    if (krb5int_buf_len(buf) > 0)
+        krb5int_buf_add(buf, "  ");
+}
 
-    /*
-    ** see if it is an error
-    */
-    if (krb5_is_krb_error(packet)) {
-        krb5_error *krberror;
-        if ((ret = krb5_rd_error(context, packet, &krberror)))
-            return(ret);
-        if (krberror->e_data.data  == NULL) {
-            ret = ERROR_TABLE_BASE_krb5 + (krb5_error_code) krberror->error;
-            krb5_free_error(context, krberror);
-            return (ret);
-        }
-        clearresult = krberror->e_data;
-        krberror->e_data.data  = NULL; /*So we can free it later*/
-        krberror->e_data.length = 0;
-        krb5_free_error(context, krberror);
-        ap_rep.length = 0;
-
-    } else { /* Not an error*/
-
-        /*
-        ** validate the message length -
-        ** length is big endian
-        */
-        message_length = (((ptr[0] << 8)&0xff) | (ptr[1]&0xff));
-        ptr += 2;
-        /*
-        ** make sure the message length and packet length agree -
-        */
-        if (message_length != packet->length)
-            return(KRB5KRB_AP_ERR_MODIFIED);
-        /*
-        ** get the version number -
-        */
-        version_number = (((ptr[0] << 8)&0xff) | (ptr[1]&0xff));
-        ptr += 2;
-        /*
-        ** make sure we support the version returned -
-        */
-        /*
-        ** set password version is 0xff80, change password version is 1
-        */
-        if (version_number != 1 && version_number != 0xff80)
-            return(KRB5KDC_ERR_BAD_PVNO);
-        /*
-        ** now fill in ap_rep with the reply -
-        */
-        /*
-        ** get the reply length -
-        */
-        ap_rep.length = (((ptr[0] << 8)&0xff) | (ptr[1]&0xff));
-        ptr += 2;
-        /*
-        ** validate ap_rep length agrees with the packet length -
-        */
-        if (ptr + ap_rep.length >= packet->data + packet->length)
-            return(KRB5KRB_AP_ERR_MODIFIED);
-        /*
-        ** if data was returned, set the ap_rep ptr -
-        */
-        if (ap_rep.length) {
-            ap_rep.data = ptr;
-            ptr += ap_rep.length;
-
-            /*
-             * Save send_subkey to later smash recv_subkey.
-             */
-            ret = krb5_auth_con_getsendsubkey(context, auth_context, &tmpkey);
-            if (ret)
-                return ret;
-
-            ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc);
-            if (ret) {
-                krb5_free_keyblock(context, tmpkey);
-                return(ret);
-            }
-
-            krb5_free_ap_rep_enc_part(context, ap_rep_enc);
-            /*
-            ** now decrypt the result -
-            */
-            cipherresult.data = ptr;
-            cipherresult.length = (packet->data + packet->length) - ptr;
-
-            /*
-             * Smash recv_subkey to be send_subkey, per spec.
-             */
-            ret = krb5_auth_con_setrecvsubkey(context, auth_context, tmpkey);
-            krb5_free_keyblock(context, tmpkey);
-            if (ret)
-                return ret;
-
-            ret = krb5_rd_priv(context, auth_context, &cipherresult, &clearresult,
-                               NULL);
-            if (ret)
-                return(ret);
-        } /*We got an ap_rep*/
-        else
-            return (KRB5KRB_AP_ERR_MODIFIED);
-    } /*Response instead of error*/
+static krb5_error_code
+decode_ad_policy_info(const krb5_data *data, char **msg_out)
+{
+    struct ad_policy_info policy;
+    uint64_t password_days;
+    const char *p;
+    char *msg;
+    struct k5buf buf;
+
+    *msg_out = NULL;
+    if (data->length != AD_POLICY_INFO_LENGTH)
+        return 0;
+
+    p = data->data;
+    policy.zero_bytes = load_16_be(p);
+    p += 2;
+
+    /* first two bytes are zeros */
+    if (policy.zero_bytes != 0)
+        return 0;
+
+    /* Read in the rest of structure */
+    policy.min_length_password = load_32_be(p);
+    p += 4;
+    policy.password_history = load_32_be(p);
+    p += 4;
+    policy.password_properties = load_32_be(p);
+    p += 4;
+    policy.expire = load_64_be(p);
+    p += 8;
+    policy.min_passwordage = load_64_be(p);
+    p += 8;
+
+    /* Check that we processed exactly the expected number of bytes. */
+    assert(p == data->data + AD_POLICY_INFO_LENGTH);
+
+    krb5int_buf_init_dynamic(&buf);
 
     /*
-    ** validate the cleartext length
-    */
-    if (clearresult.length < 2) {
-        ret = KRB5KRB_AP_ERR_MODIFIED;
-        goto cleanup;
+     * Update src/tests/misc/test_chpw_message.c if changing these strings!
+     */
+
+    if (policy.password_properties & AD_POLICY_COMPLEX) {
+        krb5int_buf_add(&buf,
+                        _("The password must include numbers or symbols.  "
+                          "Don't include any part of your name in the "
+                          "password."));
     }
-    /*
-    ** now decode the result -
-    */
-    ptr = clearresult.data;
-
-    *result_code = (((ptr[0] << 8)&0xff) | (ptr[1]&0xff));
-    ptr += 2;
-
-    /*
-    ** result code 5 is access denied
-    */
-    if ((*result_code < KRB5_KPASSWD_SUCCESS) || (*result_code > 5)) {
-        ret = KRB5KRB_AP_ERR_MODIFIED;
-        goto cleanup;
+    if (policy.min_length_password > 0) {
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password must contain at least %d "
+                                     "character.",
+                                     "The password must contain at least %d "
+                                     "characters.",
+                                     policy.min_length_password),
+                            policy.min_length_password);
     }
-    /*
-    ** all success replies should be authenticated/encrypted
-    */
-    if ((ap_rep.length == 0) && (*result_code == KRB5_KPASSWD_SUCCESS)) {
-        ret = KRB5KRB_AP_ERR_MODIFIED;
-        goto cleanup;
+    if (policy.password_history) {
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password must be different from the "
+                                     "previous password.",
+                                     "The password must be different from the "
+                                     "previous %d passwords.",
+                                     policy.password_history),
+                            policy.password_history);
     }
-
-    if (result_data) {
-        result_data->length = (clearresult.data + clearresult.length) - ptr;
-
-        if (result_data->length) {
-            result_data->data = (char *) malloc(result_data->length);
-            if (result_data->data)
-                memcpy(result_data->data, ptr, result_data->length);
-        } else
-            result_data->data = NULL;
+    if (policy.min_passwordage) {
+        password_days = policy.min_passwordage / AD_POLICY_TIME_TO_DAYS;
+        if (password_days == 0)
+            password_days = 1;
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password can only be changed once a "
+                                     "day.",
+                                     "The password can only be changed every "
+                                     "%d days.", (int)password_days),
+                            (int)password_days);
     }
-    ret = 0;
 
-cleanup:
-    krb5_free_data_contents(context, &clearresult);
-    return(ret);
+    msg = krb5int_buf_data(&buf);
+    if (msg == NULL)
+        return ENOMEM;
+
+    if (*msg != '\0')
+        *msg_out = msg;
+    else
+        free(msg);
+    return 0;
 }
 
-krb5_error_code
-krb5int_setpw_result_code_string(krb5_context context, int result_code,
-                                 const char **code_string)
+krb5_error_code KRB5_CALLCONV
+krb5_chpw_message(krb5_context context, const krb5_data *server_string,
+                  char **message_out)
 {
-    switch (result_code) {
-    case KRB5_KPASSWD_MALFORMED:
-        *code_string = "Malformed request error";
-        break;
-    case KRB5_KPASSWD_HARDERROR:
-        *code_string = "Server error";
-        break;
-    case KRB5_KPASSWD_AUTHERROR:
-        *code_string = "Authentication error";
-        break;
-    case KRB5_KPASSWD_SOFTERROR:
-        *code_string = "Password change rejected";
-        break;
-    case 5: /* access denied */
-        *code_string = "Access denied";
-        break;
-    case 6:     /* bad version */
-        *code_string = "Wrong protocol version";
-        break;
-    case 7: /* initial flag is needed */
-        *code_string = "Initial password required";
-        break;
-    case 0:
-        *code_string = "Success";
-        break;
-    default:
-        *code_string = "Password change failed";
-        break;
+    krb5_error_code ret;
+    krb5_data *string;
+    char *msg;
+
+    *message_out = NULL;
+
+    /* If server_string contains an AD password policy, construct a message
+     * based on that. */
+    ret = decode_ad_policy_info(server_string, &msg);
+    if (ret == 0 && msg != NULL) {
+        *message_out = msg;
+        return 0;
     }
 
-    return(0);
+    /* If server_string contains a valid UTF-8 string, return that. */
+    if (server_string->length > 0 &&
+        memchr(server_string->data, 0, server_string->length) == NULL &&
+        krb5int_utf8_normalize(server_string, &string,
+                               KRB5_UTF8_APPROX) == 0) {
+        *message_out = string->data; /* already null terminated */
+        free(string);
+        return 0;
+    }
+
+    /* server_string appears invalid, so try to be helpful. */
+    msg = strdup(_("Try a more complex password, or contact your "
+                   "administrator."));
+    if (msg == NULL)
+        return ENOMEM;
+
+    *message_out = msg;
+    return 0;
 }