gic_pwd.c revision 5e01956f3000408c2a2c5a08c8d0acf2c2a9d8ee
/*
* Copyright (c) 1999, 2010, Oracle and/or its affiliates. All rights reserved. */
#include "k5-int.h"
#include "com_err.h"
#include <admin.h>
#include <locale.h>
#include <syslog.h>
/* Solaris Kerberos:
*
* Change Password functionality is handled by the libkadm5clnt.so.1 library in
* Solaris Kerberos. In order to avoid a circular dependency between that lib
* and the kerberos mech lib, we use the #pragma weak compiler directive.
* This way, when applications link with the libkadm5clnt.so.1 lib the circular
* dependancy between the two libs will be resolved.
*/
#pragma weak kadm5_get_cpw_host_srv_name
#pragma weak kadm5_init_with_password
#pragma weak kadm5_chpass_principal_util
extern kadm5_ret_t kadm5_get_cpw_host_srv_name(krb5_context, const char *,
char **);
extern kadm5_ret_t kadm5_init_with_password(char *, char *, char *,
kadm5_config_params *, krb5_ui_4, krb5_ui_4, char **,
void **);
extern kadm5_ret_t kadm5_chpass_principal_util(void *, krb5_principal,
char *, char **, char *, unsigned int);
/*
* Solaris Kerberos:
* See the function's definition for the description of this interface.
*/
krb5_error_code __krb5_get_init_creds_password(krb5_context,
krb5_creds *, krb5_principal, char *, krb5_prompter_fct, void *,
krb5_deltat, char *, krb5_get_init_creds_opt *, krb5_kdc_rep **);
static krb5_error_code
krb5_get_as_key_password(
krb5_context context,
krb5_principal client,
krb5_enctype etype,
krb5_prompter_fct prompter,
void *prompter_data,
krb5_data *salt,
krb5_data *params,
krb5_keyblock *as_key,
void *gak_data)
{
krb5_data *password;
krb5_error_code ret;
krb5_data defsalt;
char *clientstr;
char promptstr[1024];
krb5_prompt prompt;
krb5_prompt_type prompt_type;
password = (krb5_data *) gak_data;
/* If there's already a key of the correct etype, we're done.
If the etype is wrong, free the existing key, and make
a new one.
XXX This was the old behavior, and was wrong in hw preauth
cases. Is this new behavior -- always asking -- correct in all
cases? */
if (as_key->length) {
if (as_key->enctype != etype) {
krb5_free_keyblock_contents (context, as_key);
as_key->length = 0;
}
}
if (password->data[0] == '\0') {
if (prompter == NULL)
prompter = krb5_prompter_posix; /* Solaris Kerberos */
if ((ret = krb5_unparse_name(context, client, &clientstr)))
return(ret);
strcpy(promptstr, "Password for ");
strncat(promptstr, clientstr, sizeof(promptstr)-strlen(promptstr)-1);
promptstr[sizeof(promptstr)-1] = '\0';
free(clientstr);
prompt.prompt = promptstr;
prompt.hidden = 1;
prompt.reply = password;
prompt_type = KRB5_PROMPT_TYPE_PASSWORD;
/* PROMPTER_INVOCATION */
krb5int_set_prompt_types(context, &prompt_type);
if ((ret = (((*prompter)(context, prompter_data, NULL, NULL,
1, &prompt))))) {
krb5int_set_prompt_types(context, 0);
return(ret);
}
krb5int_set_prompt_types(context, 0);
}
if ((salt->length == -1 || salt->length == SALT_TYPE_AFS_LENGTH) && (salt->data == NULL)) {
if ((ret = krb5_principal2salt(context, client, &defsalt)))
return(ret);
salt = &defsalt;
} else {
defsalt.length = 0;
}
ret = krb5_c_string_to_key_with_params(context, etype, password, salt,
params->data?params:NULL, as_key);
if (defsalt.length)
krb5_xfree(defsalt.data);
return(ret);
}
krb5_error_code KRB5_CALLCONV
krb5_get_init_creds_password(krb5_context context,
krb5_creds *creds,
krb5_principal client,
char *password,
krb5_prompter_fct prompter,
void *data,
krb5_deltat start_time,
char *in_tkt_service,
krb5_get_init_creds_opt *options)
{
/*
* Solaris Kerberos:
* We call our own private function that returns the as_reply back to
* the caller. This structure contains information, such as
* key-expiration and last-req fields. Entities such as pam_krb5 can
* use this information to provide account/password expiration warnings.
* The original "prompter" interface is not granular enough for PAM,
* as it will perform all passes w/o coordination with other modules.
*/
return (__krb5_get_init_creds_password(context, creds, client, password,
prompter, data, start_time, in_tkt_service, options, NULL));
}
/*
* Solaris Kerberos:
* See krb5_get_init_creds_password()'s comments for the justification of this
* private function. Caller must free ptr_as_reply if non-NULL.
*/
krb5_error_code KRB5_CALLCONV
__krb5_get_init_creds_password(
krb5_context context,
krb5_creds *creds,
krb5_principal client,
char *password,
krb5_prompter_fct prompter,
void *data,
krb5_deltat start_time,
char *in_tkt_service,
krb5_get_init_creds_opt *options,
krb5_kdc_rep **ptr_as_reply)
{
krb5_error_code ret, ret2;
int use_master;
krb5_kdc_rep *as_reply;
int tries;
krb5_creds chpw_creds;
krb5_data pw0, pw1;
char banner[1024], pw0array[1024], pw1array[1024];
krb5_prompt prompt[2];
krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
krb5_gic_opt_ext *opte = NULL;
krb5_gic_opt_ext *chpw_opte = NULL;
char admin_realm[1024], *cpw_service=NULL, *princ_str=NULL;
kadm5_config_params params;
void *server_handle;
const char *err_msg_1 = NULL;
use_master = 0;
as_reply = NULL;
memset(&chpw_creds, 0, sizeof(chpw_creds));
pw0.data = pw0array;
if (password && password[0]) {
if ((pw0.length = strlen(password)) > sizeof(pw0array)) {
ret = EINVAL;
goto cleanup;
}
strcpy(pw0.data, password);
} else {
pw0.data[0] = '\0';
pw0.length = sizeof(pw0array);
}
pw1.data = pw1array;
pw1.data[0] = '\0';
pw1.length = sizeof(pw1array);
ret = krb5int_gic_opt_to_opte(context, options, &opte, 1,
"krb5_get_init_creds_password");
if (ret)
goto cleanup;
/* first try: get the requested tkt from any kdc */
ret = krb5_get_init_creds(context, creds, client, prompter, data,
start_time, in_tkt_service, opte,
krb5_get_as_key_password, (void *) &pw0,
&use_master, &as_reply);
/* check for success */
if (ret == 0)
goto cleanup;
/* If all the kdc's are unavailable, or if the error was due to a
user interrupt, or preauth errored out, fail */
if ((ret == KRB5_KDC_UNREACH) ||
(ret == KRB5_PREAUTH_FAILED) ||
(ret == KRB5_LIBOS_PWDINTR) ||
(ret == KRB5_REALM_CANT_RESOLVE))
goto cleanup;
/* if the reply did not come from the master kdc, try again with
the master kdc */
if (!use_master) {
use_master = 1;
if (as_reply) {
krb5_free_kdc_rep( context, as_reply);
as_reply = NULL;
}
err_msg_1 = krb5_get_error_message(context, ret);
ret2 = krb5_get_init_creds(context, creds, client, prompter, data,
start_time, in_tkt_service, opte,
krb5_get_as_key_password, (void *) &pw0,
&use_master, &as_reply);
if (ret2 == 0) {
ret = 0;
goto cleanup;
}
/* if the master is unreachable, return the error from the
slave we were able to contact or reset the use_master flag */
if ((ret2 != KRB5_KDC_UNREACH) &&
(ret2 != KRB5_REALM_CANT_RESOLVE) &&
(ret2 != KRB5_REALM_UNKNOWN)) {
ret = ret2;
} else {
use_master = 0;
/* Solaris - if 2nd try failed, reset 1st err msg */
if (ret2 && err_msg_1) {
krb5_set_error_message(context, ret, err_msg_1);
}
}
}
/* Solaris Kerberos: 163 resync */
/* #ifdef USE_LOGIN_LIBRARY */
if (ret == KRB5KDC_ERR_KEY_EXP)
goto cleanup; /* Login library will deal appropriately with this error */
/* #endif */
/* at this point, we have an error from the master. if the error
is not password expired, or if it is but there's no prompter,
return this error */
if ((ret != KRB5KDC_ERR_KEY_EXP) ||
(prompter == NULL))
goto cleanup;
/* historically the default has been to prompt for password change.
* if the change password prompt option has not been set, we continue
* to prompt. Prompting is only disabled if the option has been set
* and the value has been set to false.
*/
if (!(options->flags & KRB5_GET_INIT_CREDS_OPT_CHG_PWD_PRMPT))
goto cleanup;
/* ok, we have an expired password. Give the user a few chances
to change it */
/*
* Solaris Kerberos:
* Get the correct change password service principal name to use.
* This is necessary because SEAM based admin servers require
* a slightly different service principal name than MIT/MS servers.
*/
memset((char *) &params, 0, sizeof (params));
snprintf(admin_realm, sizeof (admin_realm),
krb5_princ_realm(context, client)->data);
params.mask |= KADM5_CONFIG_REALM;
params.realm = admin_realm;
ret=kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service);
if (ret != KADM5_OK) {
syslog(LOG_ERR, dgettext(TEXT_DOMAIN,
"Kerberos mechanism library: Unable to get change password "
"service name for realm %s\n"), admin_realm);
goto cleanup;
} else {
ret=0;
}
/* extract the string version of the principal */
if ((ret = krb5_unparse_name(context, client, &princ_str)))
goto cleanup;
ret = kadm5_init_with_password(princ_str, pw0array, cpw_service,
&params, KADM5_STRUCT_VERSION, KADM5_API_VERSION_2, NULL,
&server_handle);
if (ret != 0) {
goto cleanup;
}
prompt[0].prompt = "Enter new password";
prompt[0].hidden = 1;
prompt[0].reply = &pw0;
prompt_types[0] = KRB5_PROMPT_TYPE_NEW_PASSWORD;
prompt[1].prompt = "Enter it again";
prompt[1].hidden = 1;
prompt[1].reply = &pw1;
prompt_types[1] = KRB5_PROMPT_TYPE_NEW_PASSWORD_AGAIN;
strcpy(banner, "Password expired. You must change it now.");
for (tries = 3; tries; tries--) {
pw0.length = sizeof(pw0array);
pw1.length = sizeof(pw1array);
/* PROMPTER_INVOCATION */
krb5int_set_prompt_types(context, prompt_types);
if ((ret = ((*prompter)(context, data, 0, banner,
sizeof(prompt)/sizeof(prompt[0]), prompt))))
goto cleanup;
krb5int_set_prompt_types(context, 0);
if (strcmp(pw0.data, pw1.data) != 0) {
ret = KRB5_LIBOS_BADPWDMATCH;
sprintf(banner, "%s. Please try again.", error_message(ret));
} else if (pw0.length == 0) {
ret = KRB5_CHPW_PWDNULL;
sprintf(banner, "%s. Please try again.", error_message(ret));
} else {
int result_code;
krb5_data code_string;
krb5_data result_string;
if ((ret = krb5_change_password(context, &chpw_creds, pw0array,
&result_code, &code_string,
&result_string)))
goto cleanup;
/* the change succeeded. go on */
if (result_code == 0) {
krb5_xfree(result_string.data);
break;
}
/* set this in case the retry loop falls through */
ret = KRB5_CHPW_FAIL;
if (result_code != KRB5_KPASSWD_SOFTERROR) {
krb5_xfree(result_string.data);
goto cleanup;
}
/* the error was soft, so try again */
/* 100 is I happen to know that no code_string will be longer
than 100 chars */
if (result_string.length > (sizeof(banner)-100))
result_string.length = sizeof(banner)-100;
sprintf(banner, "%.*s%s%.*s. Please try again.\n",
(int) code_string.length, code_string.data,
result_string.length ? ": " : "",
(int) result_string.length,
result_string.data ? result_string.data : "");
krb5_xfree(code_string.data);
krb5_xfree(result_string.data);
}
}
if (ret)
goto cleanup;
/* the password change was successful. Get an initial ticket
from the master. this is the last try. the return from this
is final. */
ret = krb5_get_init_creds(context, creds, client, prompter, data,
start_time, in_tkt_service, opte,
krb5_get_as_key_password, (void *) &pw0,
&use_master, &as_reply);
cleanup:
if (err_msg_1)
free((void *)err_msg_1);
krb5int_set_prompt_types(context, 0);
/* if getting the password was successful, then check to see if the
password is about to expire, and warn if so */
if (ret == 0) {
krb5_timestamp now;
krb5_last_req_entry **last_req;
int hours;
/* XXX 7 days should be configurable. This is all pretty ad hoc,
and could probably be improved if I was willing to screw around
with timezones, etc. */
if (prompter &&
(in_tkt_service && cpw_service &&
(strcmp(in_tkt_service, cpw_service) != 0)) &&
((ret = krb5_timeofday(context, &now)) == 0) &&
as_reply->enc_part2->key_exp &&
((hours = ((as_reply->enc_part2->key_exp-now)/(60*60))) <= 7*24) &&
(hours >= 0)) {
if (hours < 1)
sprintf(banner,
"Warning: Your password will expire in less than one hour.");
else if (hours <= 48)
sprintf(banner, "Warning: Your password will expire in %d hour%s.",
hours, (hours == 1)?"":"s");
else
sprintf(banner, "Warning: Your password will expire in %d days.",
hours/24);
/* ignore an error here */
/* PROMPTER_INVOCATION */
(*prompter)(context, data, 0, banner, 0, 0);
} else if (prompter &&
(!in_tkt_service ||
(strcmp(in_tkt_service, "kadmin/changepw") != 0)) &&
as_reply->enc_part2 && as_reply->enc_part2->last_req) {
/*
* Check the last_req fields
*/
for (last_req = as_reply->enc_part2->last_req; *last_req; last_req++)
if ((*last_req)->lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
(*last_req)->lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
krb5_deltat delta;
char ts[256];
if ((ret = krb5_timeofday(context, &now)))
break;
if ((ret = krb5_timestamp_to_string((*last_req)->value,
ts, sizeof(ts))))
break;
delta = (*last_req)->value - now;
if (delta < 3600)
sprintf(banner,
"Warning: Your password will expire in less than one "
"hour on %s", ts);
else if (delta < 86400*2)
sprintf(banner,
"Warning: Your password will expire in %d hour%s on %s",
delta / 3600, delta < 7200 ? "" : "s", ts);
else
sprintf(banner,
"Warning: Your password will expire in %d days on %s",
delta / 86400, ts);
/* ignore an error here */
/* PROMPTER_INVOCATION */
(*prompter)(context, data, 0, banner, 0, 0);
}
}
}
free(cpw_service);
free(princ_str);
if (opte && krb5_gic_opt_is_shadowed(opte))
krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
memset(pw0array, 0, sizeof(pw0array));
memset(pw1array, 0, sizeof(pw1array));
krb5_free_cred_contents(context, &chpw_creds);
/*
* Solaris Kerberos:
* Argument, ptr_as_reply, being returned to caller if success and non-NULL.
*/
if (as_reply != NULL) {
if (ptr_as_reply == NULL)
krb5_free_kdc_rep(context, as_reply);
else
*ptr_as_reply = as_reply;
}
return(ret);
}
krb5_error_code krb5int_populate_gic_opt (
krb5_context context, krb5_gic_opt_ext **opte,
krb5_flags options, krb5_address * const *addrs, krb5_enctype *ktypes,
krb5_preauthtype *pre_auth_types, krb5_creds *creds)
{
int i;
krb5_int32 starttime;
krb5_get_init_creds_opt *opt;
krb5_get_init_creds_opt_init(opt);
if (addrs)
krb5_get_init_creds_opt_set_address_list(opt, (krb5_address **) addrs);
if (ktypes) {
for (i=0; ktypes[i]; i++);
if (i)
krb5_get_init_creds_opt_set_etype_list(opt, ktypes, i);
}
if (pre_auth_types) {
for (i=0; pre_auth_types[i]; i++);
if (i)
krb5_get_init_creds_opt_set_preauth_list(opt, pre_auth_types, i);
}
if (options&KDC_OPT_FORWARDABLE)
krb5_get_init_creds_opt_set_forwardable(opt, 1);
else krb5_get_init_creds_opt_set_forwardable(opt, 0);
if (options&KDC_OPT_PROXIABLE)
krb5_get_init_creds_opt_set_proxiable(opt, 1);
else krb5_get_init_creds_opt_set_proxiable(opt, 0);
if (creds && creds->times.endtime) {
krb5_timeofday(context, &starttime);
if (creds->times.starttime) starttime = creds->times.starttime;
krb5_get_init_creds_opt_set_tkt_life(opt, creds->times.endtime - starttime);
}
return krb5int_gic_opt_to_opte(context, opt, opte, 0,
"krb5int_populate_gic_opt");
}
/*
Rewrites get_in_tkt in terms of newer get_init_creds API.
Attempts to get an initial ticket for creds->client to use server
creds->server, (realm is taken from creds->client), with options
options, and using creds->times.starttime, creds->times.endtime,
creds->times.renew_till as from, till, and rtime.
creds->times.renew_till is ignored unless the RENEWABLE option is requested.
If addrs is non-NULL, it is used for the addresses requested. If it is
null, the system standard addresses are used.
If password is non-NULL, it is converted using the cryptosystem entry
point for a string conversion routine, seeded with the client's name.
If password is passed as NULL, the password is read from the terminal,
and then converted into a key.
A succesful call will place the ticket in the credentials cache ccache.
returns system errors, encryption errors
*/
krb5_error_code KRB5_CALLCONV
krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options,
krb5_address *const *addrs, krb5_enctype *ktypes,
krb5_preauthtype *pre_auth_types,
const char *password, krb5_ccache ccache,
krb5_creds *creds, krb5_kdc_rep **ret_as_reply)
{
krb5_error_code retval;
krb5_data pw0;
char pw0array[1024];
char * server;
krb5_principal server_princ, client_princ;
int use_master = 0;
krb5_gic_opt_ext *opte = NULL;
pw0array[0] = '\0';
pw0.data = pw0array;
if (password) {
pw0.length = strlen(password);
if (pw0.length > sizeof(pw0array))
return EINVAL;
strncpy(pw0.data, password, sizeof(pw0array));
if (pw0.length == 0)
pw0.length = sizeof(pw0array);
} else {
pw0.length = sizeof(pw0array);
}
retval = krb5int_populate_gic_opt(context, &opte,
options, addrs, ktypes,
pre_auth_types, creds);
if (retval)
return (retval);
retval = krb5_unparse_name( context, creds->server, &server);
if (retval) {
return (retval);
krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
}
server_princ = creds->server;
client_princ = creds->client;
retval = krb5_get_init_creds (context,
creds, creds->client,
krb5_prompter_posix, NULL,
0, server, opte,
krb5_get_as_key_password, &pw0,
&use_master, ret_as_reply);
krb5_free_unparsed_name( context, server);
krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte);
if (retval) {
return (retval);
}
if (creds->server)
krb5_free_principal( context, creds->server);
if (creds->client)
krb5_free_principal( context, creds->client);
creds->client = client_princ;
creds->server = server_princ;
/* store it in the ccache! */
if (ccache)
if ((retval = krb5_cc_store_cred(context, ccache, creds)))
return (retval);
return retval;
}