Add API to interpret changepw result strings
[krb5.git] / src / lib / krb5 / krb / chpw.c
index 248c4c88c3a287a84a547dd5e0cb1e7f9ee1708b..beb77cb986f9f10a8879df776900d00d5254db66 100644 (file)
@@ -1,15 +1,21 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
 /*
 ** set password functions added by Paul W. Nelson, Thursby Software Systems, Inc.
 */
 #include <string.h>
 
 #include "k5-int.h"
-#include "krb5_err.h"
+#include "k5-unicode.h"
+#include "int-proto.h"
 #include "auth_con.h"
 
 
-krb5_error_code 
-krb5int_mk_chpw_req(krb5_context context, krb5_auth_context auth_context, krb5_data *ap_req, char *passwd, krb5_data *packet)
+krb5_error_code
+krb5int_mk_chpw_req(krb5_context context,
+                    krb5_auth_context auth_context,
+                    krb5_data *ap_req,
+                    char *passwd,
+                    krb5_data *packet)
 {
     krb5_error_code ret = 0;
     krb5_data clearpw;
@@ -20,29 +26,28 @@ krb5int_mk_chpw_req(krb5_context context, krb5_auth_context auth_context, krb5_d
     cipherpw.data = NULL;
 
     if ((ret = krb5_auth_con_setflags(context, auth_context,
-                                     KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
-         goto cleanup;
+                                      KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
+        goto cleanup;
 
     clearpw.length = strlen(passwd);
     clearpw.data = passwd;
 
     if ((ret = krb5_mk_priv(context, auth_context,
-                           &clearpw, &cipherpw, &replay)))
-      goto cleanup;
+                            &clearpw, &cipherpw, &replay)))
+        goto cleanup;
 
     packet->length = 6 + ap_req->length + cipherpw.length;
     packet->data = (char *) malloc(packet->length);
-    if (packet->data == NULL)
-         {
-           ret = ENOMEM;
-           goto cleanup;
-         }
+    if (packet->data == NULL) {
+        ret = ENOMEM;
+        goto cleanup;
+    }
     ptr = packet->data;
 
     /* length */
 
-    *ptr++ = (packet->length>>8) & 0xff;
-    *ptr++ = packet->length & 0xff;
+    store_16_be(packet->length, ptr);
+    ptr += 2;
 
     /* version == 0x0001 big-endian */
 
@@ -51,8 +56,8 @@ krb5int_mk_chpw_req(krb5_context context, krb5_auth_context auth_context, krb5_d
 
     /* ap_req length, big-endian */
 
-    *ptr++ = (ap_req->length>>8) & 0xff;
-    *ptr++ = ap_req->length & 0xff;
+    store_16_be(ap_req->length, ptr);
+    ptr += 2;
 
     /* ap-req data */
 
@@ -64,437 +69,449 @@ krb5int_mk_chpw_req(krb5_context context, krb5_auth_context auth_context, krb5_d
     memcpy(ptr, cipherpw.data, cipherpw.length);
 
 cleanup:
-    if(cipherpw.data != NULL)  /* allocated by krb5_mk_priv */
-      free(cipherpw.data);
-      
+    if (cipherpw.data != NULL)  /* allocated by krb5_mk_priv */
+        free(cipherpw.data);
+
     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;
-    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;
-    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;
-
-    /* verify length */
-
-    plen = (*ptr++ & 0xff);
-    plen = (plen<<8) | (*ptr++ & 0xff);
-
-    if (plen != packet->length)
-       return(KRB5KRB_AP_ERR_MODIFIED);
-
-    /* verify version number */
+    krb5_error *krberror = NULL;
 
-    vno = (*ptr++ & 0xff);
-    vno = (vno<<8) | (*ptr++ & 0xff);
-
-    if (vno != 1)
-       return(KRB5KDC_ERR_BAD_PVNO);
-
-    /* read, check ap-rep length */
-
-    ap_rep.length = (*ptr++ & 0xff);
-    ap_rep.length = (ap_rep.length<<8) | (*ptr++ & 0xff);
+    *edata_out = NULL;
 
-    if (ptr + ap_rep.length >= packet->data + packet->length)
-       return(KRB5KRB_AP_ERR_MODIFIED);
+    ret = krb5_rd_error(context, error_packet, &krberror);
+    if (ret)
+        return ret;
 
-    if (ap_rep.length) {
-       /* verify ap_rep */
-       ap_rep.data = ptr;
-       ptr += ap_rep.length;
-
-       if ((ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc)))
-           return(ret);
+    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;
+    }
 
-       krb5_free_ap_rep_enc_part(context, ap_rep_enc);
+    ret = krb5_copy_data(context, &krberror->e_data, edata_out);
 
-       /* extract and decrypt the result */
+cleanup:
+    krb5_free_error(context, krberror);
+    return ret;
+}
 
-       cipherresult.data = ptr;
-       cipherresult.length = (packet->data + packet->length) - ptr;
+/* 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();
 
-       /* XXX there's no api to do this right. The problem is that
-          if there's a remote subkey, it will be used.  This is
-          not what the spec requires */
+    *clear_out = NULL;
+    *is_error_out = FALSE;
 
-       tmp = auth_context->remote_subkey;
-       auth_context->remote_subkey = NULL;
+    /* 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);
+    }
 
-       ret = krb5_rd_priv(context, auth_context, &cipherresult, &clearresult,
-                          &replay);
+    if (packet->length < 6)
+        return KRB5KRB_AP_ERR_MODIFIED;
 
-       auth_context->remote_subkey = tmp;
+    /* 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;
 
-       if (ret)
-           return(ret);
-    } else {
-       cipherresult.data = ptr;
-       cipherresult.length = (packet->data + packet->length) - ptr;
+    /* 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);
+    }
 
-       if ((ret = krb5_rd_error(context, &cipherresult, &krberror)))
-           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;
+
+    /* 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);
+
+    /* Smash recv_subkey to be send_subkey, per spec. */
+    ret = krb5_auth_con_setrecvsubkey_k(context, auth_context, send_subkey);
+    if (ret)
+        goto cleanup;
+
+    /* 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;
+
+    ret = krb5_copy_data(context, &clear, clear_out);
+    if (ret)
+        goto cleanup;
+    *is_error_out = FALSE;
 
-       clearresult = krberror->e_data;
-    }
+cleanup:
+    krb5_k_free_key(context, send_subkey);
+    krb5_free_data_contents(context, &clear);
+    return ret;
+}
 
-    if (clearresult.length < 2) {
-       ret = KRB5KRB_AP_ERR_MODIFIED;
-       goto cleanup;
-    }
+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;
 
-    ptr = clearresult.data;
+    *result_code_out = 0;
+    *result_data_out = empty_data();
 
-    *result_code = (*ptr++ & 0xff);
-    *result_code = (*result_code<<8) | (*ptr++ & 0xff);
+    ret = get_clear_result(context, auth_context, packet, &clear, &is_error);
+    if (ret)
+        return ret;
 
-    if ((*result_code < KRB5_KPASSWD_SUCCESS) ||
-       (*result_code > KRB5_KPASSWD_SOFTERROR)) {
-       ret = KRB5KRB_AP_ERR_MODIFIED;
-       goto cleanup;
+    if (clear->length < 2) {
+        ret = KRB5KRB_AP_ERR_MODIFIED;
+        goto cleanup;
     }
 
-    /* all success replies should be authenticated/encrypted */
-
-    if ((ap_rep.length == 0) && (*result_code == KRB5_KPASSWD_SUCCESS)) {
-       ret = KRB5KRB_AP_ERR_MODIFIED;
-       goto cleanup;
+    /* 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;
     }
 
-    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;
+    /* Successful replies must not come from errors. */
+    if (is_error && result_code == KRB5_KPASSWD_SUCCESS) {
+        ret = KRB5KRB_AP_ERR_MODIFIED;
+        goto cleanup;
     }
 
-    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) {
-       krb5_xfree(clearresult.data);
-    } else {
-       krb5_free_error(context, krberror);
-    }
-
-    return(ret);
+    krb5_free_data(context, clear);
+    return ret;
 }
 
 krb5_error_code KRB5_CALLCONV
-krb5_chpw_result_code_string(krb5_context context, int result_code, char **code_string)
+krb5_chpw_result_code_string(krb5_context context, int result_code,
+                             char **code_string)
 {
-   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;
-   default:
-      *code_string = "Password change failed";
-      break;
-   }
-
-   return(0);
+    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 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");
+        break;
+    }
+
+    return 0;
 }
 
-krb5_error_code 
-krb5int_mk_setpw_req(
-     krb5_context context,
-     krb5_auth_context auth_context,
-     krb5_data *ap_req,
-     krb5_principal targprinc,
-     char *passwd,
-     krb5_data *packet )
+krb5_error_code
+krb5int_mk_setpw_req(krb5_context context,
+                     krb5_auth_context auth_context,
+                     krb5_data *ap_req,
+                     krb5_principal targprinc,
+                     char *passwd,
+                     krb5_data *packet)
 {
     krb5_error_code ret;
-    krb5_data  cipherpw;
-    krb5_data  *encoded_setpw;
+    krb5_data   cipherpw;
+    krb5_data   *encoded_setpw;
+    struct krb5_setpw_req req;
 
     char *ptr;
-     int count = 2;
 
-     cipherpw.data = NULL;
-     cipherpw.length = 0;
-     
-    if (ret = krb5_auth_con_setflags(context, auth_context,
-                                    KRB5_AUTH_CONTEXT_DO_SEQUENCE))
-               return(ret);
+    cipherpw.data = NULL;
+    cipherpw.length = 0;
+
+    if ((ret = krb5_auth_con_setflags(context, auth_context,
+                                      KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
+        return(ret);
 
-    ret = encode_krb5_setpw_req(targprinc, passwd, &encoded_setpw);
+    req.target = targprinc;
+    req.password.data = passwd;
+    req.password.length = strlen(passwd);
+    ret = encode_krb5_setpw_req(&req, &encoded_setpw);
     if (ret) {
-       return ret;
+        return ret;
     }
 
-    if ( (ret = krb5_mk_priv(context, auth_context, encoded_setpw, &cipherpw, NULL)) != 0) {
-       krb5_free_data( context, encoded_setpw);
-       return(ret);
+    if ((ret = krb5_mk_priv(context, auth_context, encoded_setpw, &cipherpw, NULL)) != 0) {
+        krb5_free_data(context, encoded_setpw);
+        return(ret);
     }
-    krb5_free_data( context, encoded_setpw);
-    
+    krb5_free_data(context, encoded_setpw);
+
 
     packet->length = 6 + ap_req->length + cipherpw.length;
     packet->data = (char *) malloc(packet->length);
     if (packet->data  == NULL) {
-       ret = ENOMEM;
-       goto cleanup;
+        ret = ENOMEM;
+        goto cleanup;
     }
     ptr = packet->data;
-/*
-** build the packet -
-*/
-/* put in the length */
-    *ptr++ = (packet->length>>8) & 0xff;
-    *ptr++ = packet->length & 0xff;
-/* put in the version */
+    /*
+    ** build the packet -
+    */
+    /* put in the length */
+    store_16_be(packet->length, ptr);
+    ptr += 2;
+    /* put in the version */
     *ptr++ = (char)0xff;
     *ptr++ = (char)0x80;
-/* the ap_req length is big endian */
-    *ptr++ = (ap_req->length>>8) & 0xff;
-    *ptr++ = ap_req->length & 0xff;
-/* put in the request data */
+    /* the ap_req length is big endian */
+    store_16_be(ap_req->length, ptr);
+    ptr += 2;
+    /* put in the request data */
     memcpy(ptr, ap_req->data, ap_req->length);
     ptr += ap_req->length;
-/*
-** put in the "private" password data -
-*/
+    /*
+    ** put in the "private" password data -
+    */
     memcpy(ptr, cipherpw.data, cipherpw.length);
     ret = 0;
- cleanup:
+cleanup:
     if (cipherpw.data)
-       krb5_free_data_contents(context, &cipherpw);
+        krb5_free_data_contents(context, &cipherpw);
     if ((ret != 0) && packet->data) {
-       free( packet->data);
-       packet->data = NULL;
+        free(packet->data);
+        packet->data = NULL;
     }
     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 )
-{
-    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_replay_data replay;
-/*
-** validate the packet length -
-*/
-    if (packet->length < 4)
-       return(KRB5KRB_AP_ERR_MODIFIED);
-
-    ptr = packet->data;
-
-/*
-** 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 + 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);
-               
-    } 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 != 0xff80 && version_number != 1)
-           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;
-
-           if (ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc))
-               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;
+ * 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)
+{
+    if (krb5int_buf_len(buf) > 0)
+        krb5int_buf_add(buf, "  ");
+}
 
-           {
-               krb5_keyblock *saved_remote_subkey;
-/*
-** save the remote_subkey, so it doesn't get used when decoding 
-*/
-               saved_remote_subkey      = auth_context->remote_subkey;
-               auth_context->remote_subkey = NULL;
+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);
+
+    /*
+     * 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."));
+    }
+    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);
+    }
+    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 (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 = krb5_rd_priv(context, auth_context, &cipherresult, &clearresult,
-                                  NULL);
-               auth_context->remote_subkey = saved_remote_subkey;
-           }
+    msg = krb5int_buf_data(&buf);
+    if (msg == NULL)
+        return ENOMEM;
 
-           if (ret)
-               return(ret);
-       } /*We got an ap_rep*/
-       else
-           return (KRB5KRB_AP_ERR_MODIFIED);
-    } /*Response instead of error*/
+    if (*msg != '\0')
+        *msg_out = msg;
+    else
+        free(msg);
+    return 0;
+}
 
-/*
-** validate the cleartext length 
-*/
-    if (clearresult.length < 2) {
-       ret = KRB5KRB_AP_ERR_MODIFIED;
-       goto cleanup;
-    }
-/*
-** now decode the result -
-*/
-    ptr = clearresult.data;
+krb5_error_code KRB5_CALLCONV
+krb5_chpw_message(krb5_context context, const krb5_data *server_string,
+                  char **message_out)
+{
+    krb5_error_code ret;
+    krb5_data *string;
+    char *msg;
 
-    *result_code = (((ptr[0] << 8)&0xff) | (ptr[1]&0xff));
-    ptr += 2;
+    *message_out = NULL;
 
-/*
-** result code 5 is access denied
-*/
-    if ((*result_code < KRB5_KPASSWD_SUCCESS) || (*result_code > 5))
-    {
-       ret = KRB5KRB_AP_ERR_MODIFIED;
-       goto cleanup;
-    }
-/*
-** 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 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;
     }
 
-    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 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;
     }
-    ret = 0;
 
- cleanup:
-    krb5_free_data_contents(context, &clearresult);
-    return(ret);
-}
+    /* 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;
 
-krb5_error_code 
-krb5int_setpw_result_code_string( krb5_context context, int result_code, const char **code_string )
-{
-   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";
-   default:
-      *code_string = "Password change failed";
-      break;
-   }
-
-   return(0);
+    *message_out = msg;
+    return 0;
 }
-