+/* -*- 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_DLLIMP krb5_error_code KRB5_CALLCONV
-krb5_mk_chpw_req(context, auth_context, ap_req, passwd, packet)
- 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;
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 */
/* 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 */
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_DLLIMP krb5_error_code KRB5_CALLCONV
-krb5_rd_chpw_rep(context, auth_context, packet, result_code, result_data)
- 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)
+{
+ krb5_error_code ret;
+ krb5_error *krberror = NULL;
+
+ *edata_out = NULL;
+
+ ret = krb5_rd_error(context, error_packet, &krberror);
+ if (ret)
+ 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;
+ }
+
+ ret = krb5_copy_data(context, &krberror->e_data, edata_out);
+
+cleanup:
+ krb5_free_error(context, krberror);
+ return ret;
+}
+
+/* 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)
{
- 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;
+ 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_keyblock *tmp;
+ krb5_key send_subkey = NULL;
+ krb5_data clear = empty_data();
- if (packet->length < 4)
- /* either this, or the server is printing bad messages,
- or the caller passed in garbage */
- return(KRB5KRB_AP_ERR_MODIFIED);
+ *clear_out = NULL;
+ *is_error_out = FALSE;
- ptr = packet->data;
+ /* 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);
+ }
- /* verify length */
+ if (packet->length < 6)
+ return KRB5KRB_AP_ERR_MODIFIED;
+ /* Decode and verify the length. */
+ ptr = packet->data;
plen = (*ptr++ & 0xff);
- plen = (plen<<8) | (*ptr++ & 0xff);
-
+ plen = (plen << 8) | (*ptr++ & 0xff);
if (plen != packet->length)
- return(KRB5KRB_AP_ERR_MODIFIED);
-
- /* verify version number */
+ return KRB5KRB_AP_ERR_MODIFIED;
+ /* Decode and verify the version number. */
vno = (*ptr++ & 0xff);
- vno = (vno<<8) | (*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 (vno != 1)
- return(KRB5KDC_ERR_BAD_PVNO);
+ /* 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;
- /* read, check ap-rep length */
+cleanup:
+ krb5_k_free_key(context, send_subkey);
+ krb5_free_data_contents(context, &clear);
+ return ret;
+}
- ap_rep.length = (*ptr++ & 0xff);
- ap_rep.length = (ap_rep.length<<8) | (*ptr++ & 0xff);
+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 (ptr + ap_rep.length >= packet->data + packet->length)
- return(KRB5KRB_AP_ERR_MODIFIED);
+ *result_code_out = 0;
+ *result_data_out = empty_data();
- if (ap_rep.length) {
- /* verify ap_rep */
- ap_rep.data = ptr;
- ptr += ap_rep.length;
+ ret = get_clear_result(context, auth_context, packet, &clear, &is_error);
+ if (ret)
+ return ret;
- if ((ret = krb5_rd_rep(context, auth_context, &ap_rep, &ap_rep_enc)))
- return(ret);
+ if (clear->length < 2) {
+ ret = KRB5KRB_AP_ERR_MODIFIED;
+ goto cleanup;
+ }
- krb5_free_ap_rep_enc_part(context, ap_rep_enc);
+ /* 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;
+ }
- /* extract and decrypt the result */
+ /* Successful replies must not come from errors. */
+ if (is_error && result_code == KRB5_KPASSWD_SUCCESS) {
+ ret = KRB5KRB_AP_ERR_MODIFIED;
+ goto cleanup;
+ }
- cipherresult.data = ptr;
- cipherresult.length = (packet->data + packet->length) - ptr;
+ 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;
- /* 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 */
+cleanup:
+ krb5_free_data(context, clear);
+ return ret;
+}
- tmp = auth_context->remote_subkey;
- auth_context->remote_subkey = NULL;
+krb5_error_code KRB5_CALLCONV
+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;
+ 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;
+ }
- ret = krb5_rd_priv(context, auth_context, &cipherresult, &clearresult,
- &replay);
+ return 0;
+}
- auth_context->remote_subkey = tmp;
+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;
+ struct krb5_setpw_req req;
- if (ret)
- return(ret);
- } else {
- cipherresult.data = ptr;
- cipherresult.length = (packet->data + packet->length) - ptr;
+ char *ptr;
- if ((ret = krb5_rd_error(context, &cipherresult, &krberror)))
- return(ret);
+ cipherpw.data = NULL;
+ cipherpw.length = 0;
- clearresult = krberror->e_data;
+ if ((ret = krb5_auth_con_setflags(context, auth_context,
+ KRB5_AUTH_CONTEXT_DO_SEQUENCE)))
+ return(ret);
+
+ req.target = targprinc;
+ req.password.data = passwd;
+ req.password.length = strlen(passwd);
+ ret = encode_krb5_setpw_req(&req, &encoded_setpw);
+ if (ret) {
+ return ret;
}
- if (clearresult.length < 2) {
- ret = KRB5KRB_AP_ERR_MODIFIED;
- goto cleanup;
+ 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);
- ptr = clearresult.data;
-
- *result_code = (*ptr++ & 0xff);
- *result_code = (*result_code<<8) | (*ptr++ & 0xff);
- if ((*result_code < KRB5_KPASSWD_SUCCESS) ||
- (*result_code > KRB5_KPASSWD_SOFTERROR)) {
- ret = KRB5KRB_AP_ERR_MODIFIED;
- goto cleanup;
+ packet->length = 6 + ap_req->length + cipherpw.length;
+ packet->data = (char *) malloc(packet->length);
+ if (packet->data == NULL) {
+ ret = ENOMEM;
+ goto cleanup;
}
+ ptr = packet->data;
+ /*
+ ** 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 */
+ 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 -
+ */
+ memcpy(ptr, cipherpw.data, cipherpw.length);
+ ret = 0;
+cleanup:
+ if (cipherpw.data)
+ krb5_free_data_contents(context, &cipherpw);
+ if ((ret != 0) && packet->data) {
+ free(packet->data);
+ packet->data = NULL;
+ }
+ return ret;
+}
- /* all success replies should be authenticated/encrypted */
+/*
+ * 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, " ");
+}
- if ((ap_rep.length == 0) && (*result_code == KRB5_KPASSWD_SUCCESS)) {
- ret = KRB5KRB_AP_ERR_MODIFIED;
- goto cleanup;
+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);
}
- result_data->length = (clearresult.data + clearresult.length) - ptr;
+ msg = krb5int_buf_data(&buf);
+ if (msg == NULL)
+ return ENOMEM;
- 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;
- }
+ if (*msg != '\0')
+ *msg_out = msg;
+ else
+ free(msg);
+ return 0;
+}
- ret = 0;
+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;
-cleanup:
- if (ap_rep.length) {
- krb5_xfree(clearresult.data);
- } else {
- krb5_free_error(context, krberror);
+ *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(ret);
-}
+ /* 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;
+ }
-KRB5_DLLIMP krb5_error_code KRB5_CALLCONV
-krb5_chpw_result_code_string(context, result_code, 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);
+ /* 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;
}