/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krb5/krb/gic_keytab.c */
/*
 * Copyright (C) 2002, 2003, 2008 by the Massachusetts Institute of Technology.
 * All rights reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */
#ifndef LEAN_CLIENT

#include "k5-int.h"
#include "int-proto.h"
#include "init_creds_ctx.h"

static krb5_error_code
get_as_key_keytab(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,
                  k5_response_items *ritems)
{
    krb5_keytab keytab = (krb5_keytab) gak_data;
    krb5_error_code ret;
    krb5_keytab_entry kt_ent;
    krb5_keyblock *kt_key;

    /* We don't need the password from the responder to create the AS key. */
    if (as_key == NULL)
        return 0;

    /* 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. */

    if (as_key->length) {
        if (as_key->enctype == etype)
            return(0);

        krb5_free_keyblock_contents(context, as_key);
        as_key->length = 0;
    }

    if (!krb5_c_valid_enctype(etype))
        return(KRB5_PROG_ETYPE_NOSUPP);

    if ((ret = krb5_kt_get_entry(context, keytab, client,
                                 0, /* don't have vno available */
                                 etype, &kt_ent)))
        return(ret);

    ret = krb5_copy_keyblock(context, &kt_ent.key, &kt_key);

    /* again, krb5's memory management is lame... */

    *as_key = *kt_key;
    free(kt_key);

    (void) krb5_kt_free_entry(context, &kt_ent);

    return(ret);
}

/* Return the list of etypes available for client in keytab. */
static krb5_error_code
lookup_etypes_for_keytab(krb5_context context, krb5_keytab keytab,
                         krb5_principal client, krb5_enctype **etypes_out)
{
    krb5_kt_cursor cursor;
    krb5_keytab_entry entry;
    krb5_enctype *p, *etypes = NULL, etype;
    krb5_kvno max_kvno = 0, vno;
    krb5_error_code ret;
    krb5_boolean match;
    size_t count = 0;

    *etypes_out = NULL;

    if (keytab->ops->start_seq_get == NULL)
        return EINVAL;
    ret = krb5_kt_start_seq_get(context, keytab, &cursor);
    if (ret != 0)
        return ret;

    while (!(ret = krb5_kt_next_entry(context, keytab, &entry, &cursor))) {
        /* Extract what we need from the entry and free it. */
        etype = entry.key.enctype;
        vno = entry.vno;
        match = krb5_principal_compare(context, entry.principal, client);
        krb5_free_keytab_entry_contents(context, &entry);

        /* Filter out old or non-matching entries and invalid enctypes. */
        if (vno < max_kvno || !match || !krb5_c_valid_enctype(etype))
            continue;

        /* Update max_kvno and reset the list if we find a newer kvno. */
        if (vno > max_kvno) {
            max_kvno = vno;
            free(etypes);
            etypes = NULL;
            count = 0;
        }

        /* Leave room for the terminator and possibly a second entry. */
        p = realloc(etypes, (count + 3) * sizeof(*etypes));
        if (p == NULL) {
            ret = ENOMEM;
            goto cleanup;
        }
        etypes = p;
        etypes[count++] = etype;
        etypes[count] = 0;
    }
    if (ret != KRB5_KT_END)
        goto cleanup;
    ret = 0;

    *etypes_out = etypes;
    etypes = NULL;

cleanup:
    krb5_kt_end_seq_get(context, keytab, &cursor);
    free(etypes);
    return ret;
}

/* Move the entries in keytab_list (zero-terminated) to the front of req_list
 * (of length req_len), preserving order otherwise. */
static krb5_error_code
sort_enctypes(krb5_enctype *req_list, int req_len, krb5_enctype *keytab_list)
{
    krb5_enctype *save_list;
    int save_pos, req_pos, i;

    save_list = malloc(req_len * sizeof(*save_list));
    if (save_list == NULL)
        return ENOMEM;

    /* Sort req_list entries into the front of req_list or into save_list. */
    req_pos = save_pos = 0;
    for (i = 0; i < req_len; i++) {
        if (k5_etypes_contains(keytab_list, req_list[i]))
            req_list[req_pos++] = req_list[i];
        else
            save_list[save_pos++] = req_list[i];
    }

    /* Put the entries we saved back in at the end, in order. */
    for (i = 0; i < save_pos; i++)
        req_list[req_pos++] = save_list[i];
    assert(req_pos == req_len);

    free(save_list);
    return 0;
}

krb5_error_code KRB5_CALLCONV
krb5_init_creds_set_keytab(krb5_context context,
                           krb5_init_creds_context ctx,
                           krb5_keytab keytab)
{
    krb5_enctype *etype_list;
    krb5_error_code ret;
    char *name;

    ctx->gak_fct = get_as_key_keytab;
    ctx->gak_data = keytab;

    ret = lookup_etypes_for_keytab(context, keytab, ctx->request->client,
                                   &etype_list);
    if (ret) {
        TRACE_INIT_CREDS_KEYTAB_LOOKUP_FAILED(context, ret);
        return 0;
    }
    TRACE_INIT_CREDS_KEYTAB_LOOKUP(context, etype_list);

    /* Error out if we have no keys for the client principal. */
    if (etype_list == NULL) {
        ret = krb5_unparse_name(context, ctx->request->client, &name);
        if (ret == 0) {
            k5_setmsg(context, KRB5_KT_NOTFOUND,
                      _("Keytab contains no suitable keys for %s"), name);
        }
        krb5_free_unparsed_name(context, name);
        return KRB5_KT_NOTFOUND;
    }

    /* Sort the request enctypes so the ones in the keytab appear first. */
    ret = sort_enctypes(ctx->request->ktype, ctx->request->nktypes,
                        etype_list);
    free(etype_list);
    return ret;
}

static krb5_error_code
get_init_creds_keytab(krb5_context context, krb5_creds *creds,
                      krb5_principal client, krb5_keytab keytab,
                      krb5_deltat start_time, const char *in_tkt_service,
                      krb5_get_init_creds_opt *options, int *use_master)
{
    krb5_error_code ret;
    krb5_init_creds_context ctx = NULL;

    ret = krb5_init_creds_init(context, client, NULL, NULL, start_time,
                               options, &ctx);
    if (ret != 0)
        goto cleanup;

    if (in_tkt_service) {
        ret = krb5_init_creds_set_service(context, ctx, in_tkt_service);
        if (ret != 0)
            goto cleanup;
    }

    ret = krb5_init_creds_set_keytab(context, ctx, keytab);
    if (ret != 0)
        goto cleanup;

    ret = k5_init_creds_get(context, ctx, use_master);
    if (ret != 0)
        goto cleanup;

    ret = krb5_init_creds_get_creds(context, ctx, creds);
    if (ret != 0)
        goto cleanup;

cleanup:
    krb5_init_creds_free(context, ctx);

    return ret;
}

krb5_error_code KRB5_CALLCONV
krb5_get_init_creds_keytab(krb5_context context,
                           krb5_creds *creds,
                           krb5_principal client,
                           krb5_keytab arg_keytab,
                           krb5_deltat start_time,
                           const char *in_tkt_service,
                           krb5_get_init_creds_opt *options)
{
    krb5_error_code ret;
    int use_master;
    krb5_keytab keytab;
    struct errinfo errsave = EMPTY_ERRINFO;

    if (arg_keytab == NULL) {
        if ((ret = krb5_kt_default(context, &keytab)))
            return ret;
    } else {
        keytab = arg_keytab;
    }

    use_master = 0;

    /* first try: get the requested tkt from any kdc */

    ret = get_init_creds_keytab(context, creds, client, keytab, start_time,
                                in_tkt_service, options, &use_master);

    /* check for success */

    if (ret == 0)
        goto cleanup;

    /* If all the kdc's are unavailable fail */

    if ((ret == KRB5_KDC_UNREACH) || (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;

        k5_save_ctx_error(context, ret, &errsave);
        ret = get_init_creds_keytab(context, creds, client, keytab,
                                    start_time, in_tkt_service, options,
                                    &use_master);
        if (ret == 0)
            goto cleanup;

        /* If the master is unreachable, return the error from the replica we
         * were able to contact. */
        if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
            ret == KRB5_REALM_UNKNOWN)
            ret = k5_restore_ctx_error(context, &errsave);
    }

    /* at this point, we have a response from the master.  Since we don't
       do any prompting or changing for keytabs, that's it. */

cleanup:
    if (arg_keytab == NULL)
        krb5_kt_close(context, keytab);
    k5_clear_error(&errsave);

    return(ret);
}
krb5_error_code KRB5_CALLCONV
krb5_get_in_tkt_with_keytab(krb5_context context, krb5_flags options,
                            krb5_address *const *addrs, krb5_enctype *ktypes,
                            krb5_preauthtype *pre_auth_types,
                            krb5_keytab arg_keytab, krb5_ccache ccache,
                            krb5_creds *creds, krb5_kdc_rep **ret_as_reply)
{
    krb5_error_code retval;
    krb5_get_init_creds_opt *opts;
    char * server = NULL;
    krb5_keytab keytab;
    krb5_principal client_princ, server_princ;
    int use_master = 0;

    retval = k5_populate_gic_opt(context, &opts, options, addrs, ktypes,
                                 pre_auth_types, creds);
    if (retval)
        return retval;

    if (arg_keytab == NULL) {
        retval = krb5_kt_default(context, &keytab);
        if (retval)
            goto cleanup;
    }
    else keytab = arg_keytab;

    retval = krb5_unparse_name( context, creds->server, &server);
    if (retval)
        goto cleanup;
    server_princ = creds->server;
    client_princ = creds->client;
    retval = k5_get_init_creds(context, creds, creds->client,
                               krb5_prompter_posix,  NULL, 0, server, opts,
                               get_as_key_keytab, (void *)keytab, &use_master,
                               ret_as_reply);
    krb5_free_unparsed_name( context, server);
    if (retval) {
        goto cleanup;
    }
    krb5_free_principal(context, creds->server);
    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)))
            goto cleanup;
cleanup:
    krb5_get_init_creds_opt_free(context, opts);
    if (arg_keytab == NULL)
        krb5_kt_close(context, keytab);
    return retval;
}

#endif /* LEAN_CLIENT */
