/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 2009 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.
 *
 */

#include "k5-int.h"
#include "authdata.h"
#include "auth_con.h"
#include "int-proto.h"

/* Loosely based on preauth2.c */

#define IS_PRIMARY_INSTANCE(_module) ((_module)->client_req_init != NULL)

static const char *objdirs[] = {
#if TARGET_OS_MAC
    KRB5_AUTHDATA_PLUGIN_BUNDLE_DIR,
#endif
    LIBDIR "/krb5/plugins/authdata",
    NULL
}; /* should be a list */

/* Internal authdata systems */
static krb5plugin_authdata_client_ftable_v0 *authdata_systems[] = {
    &krb5int_mspac_authdata_client_ftable,
    &krb5int_s4u2proxy_authdata_client_ftable,
    NULL
};

static inline int
k5_ad_module_count(krb5plugin_authdata_client_ftable_v0 *table)
{
    int i;

    if (table->ad_type_list == NULL)
        return 0;

    for (i = 0; table->ad_type_list[i]; i++)
        ;

    return i;
}

static krb5_error_code
k5_ad_init_modules(krb5_context kcontext,
                   krb5_authdata_context context,
                   krb5plugin_authdata_client_ftable_v0 *table,
                   int *module_count)
{
    int j, k = *module_count;
    krb5_error_code code;
    void *plugin_context = NULL;
    void **rcpp = NULL;

    if (table->ad_type_list == NULL) {
#ifdef DEBUG
        fprintf(stderr, "warning: module \"%s\" does not advertise "
                "any AD types\n", table->name);
#endif
        return ENOENT;
    }

    if (table->init == NULL)
        return ENOSYS;

    code = (*table->init)(kcontext, &plugin_context);
    if (code != 0) {
#ifdef DEBUG
        fprintf(stderr, "warning: skipping module \"%s\" which "
                "failed to initialize\n", table->name);
#endif
        return code;
    }

    for (j = 0; table->ad_type_list[j] != 0; j++) {
        context->modules[k].ad_type = table->ad_type_list[j];
        context->modules[k].plugin_context = plugin_context;
        if (j == 0)
            context->modules[k].client_fini = table->fini;
        else
            context->modules[k].client_fini = NULL;
        context->modules[k].ftable = table;
        context->modules[k].name = table->name;
        if (table->flags != NULL) {
            (*table->flags)(kcontext, plugin_context,
                            context->modules[k].ad_type,
                            &context->modules[k].flags);
        } else {
            context->modules[k].flags = 0;
        }
        context->modules[k].request_context = NULL;
        if (j == 0) {
            context->modules[k].client_req_init = table->request_init;
            context->modules[k].client_req_fini = table->request_fini;
            rcpp = &context->modules[k].request_context;

            /* For now, single request per context. That may change */
            code = (*table->request_init)(kcontext,
                                          context,
                                          plugin_context,
                                          rcpp);
            if ((code != 0 && code != ENOMEM) &&
                (context->modules[k].flags & AD_INFORMATIONAL))
                code = 0;
            if (code != 0)
                break;
        } else {
            context->modules[k].client_req_init = NULL;
            context->modules[k].client_req_fini = NULL;
        }
        context->modules[k].request_context_pp = rcpp;

#ifdef DEBUG
        fprintf(stderr, "init module \"%s\", ad_type %d, flags %08x\n",
                context->modules[k].name,
                context->modules[k].ad_type,
                context->modules[k].flags);
#endif
        k++;
    }
    *module_count = k;

    return code;
}

/*
 * Determine size of to-be-externalized authdata context, for
 * modules that match given flags mask. Note that this size
 * does not include the magic identifier/trailer.
 */
static krb5_error_code
k5_ad_size(krb5_context kcontext,
           krb5_authdata_context context,
           krb5_flags flags,
           size_t *sizep)
{
    int i;
    krb5_error_code code = 0;

    *sizep += sizeof(krb5_int32); /* count */

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];
        size_t size;

        if ((module->flags & flags) == 0)
            continue;

        /* externalize request context for the first instance only */
        if (!IS_PRIMARY_INSTANCE(module))
            continue;

        if (module->ftable->size == NULL)
            continue;

        assert(module->ftable->externalize != NULL);

        size = sizeof(krb5_int32) /* namelen */ + strlen(module->name);

        code = (*module->ftable->size)(kcontext,
                                       context,
                                       module->plugin_context,
                                       *(module->request_context_pp),
                                       &size);
        if (code != 0)
            break;

        *sizep += size;
    }

    return code;
}

/*
 * Externalize authdata context, for modules that match given flags
 * mask. Note that the magic identifier/trailer is not included.
 */
static krb5_error_code
k5_ad_externalize(krb5_context kcontext,
                  krb5_authdata_context context,
                  krb5_flags flags,
                  krb5_octet **buffer,
                  size_t *lenremain)
{
    int i;
    krb5_error_code code;
    krb5_int32 ad_count = 0;
    krb5_octet *bp;
    size_t remain;

    bp = *buffer;
    remain = *lenremain;

    /* placeholder for count */
    code = krb5_ser_pack_int32(0, &bp, &remain);
    if (code != 0)
        return code;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];
        size_t namelen;

        if ((module->flags & flags) == 0)
            continue;

        /* externalize request context for the first instance only */
        if (!IS_PRIMARY_INSTANCE(module))
            continue;

        if (module->ftable->externalize == NULL)
            continue;

        /*
         * We use the module name rather than the authdata type, because
         * there may be multiple modules for a particular authdata type.
         */
        namelen = strlen(module->name);

        code = krb5_ser_pack_int32((krb5_int32)namelen, &bp, &remain);
        if (code != 0)
            break;

        code = krb5_ser_pack_bytes((krb5_octet *)module->name,
                                   namelen, &bp, &remain);
        if (code != 0)
            break;

        code = (*module->ftable->externalize)(kcontext,
                                              context,
                                              module->plugin_context,
                                              *(module->request_context_pp),
                                              &bp,
                                              &remain);
        if (code != 0)
            break;

        ad_count++;
    }

    if (code == 0) {
        /* store actual count */
        krb5_ser_pack_int32(ad_count, buffer, lenremain);

        *buffer = bp;
        *lenremain = remain;
    }

    return code;
}

/*
 * Find authdata module for authdata type that matches flag mask
 */
static struct _krb5_authdata_context_module *
k5_ad_find_module(krb5_context kcontext,
                  krb5_authdata_context context,
                  krb5_flags flags,
                  const krb5_data *name)
{
    int i;
    struct _krb5_authdata_context_module *ret = NULL;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];

        if ((module->flags & flags) == 0)
            continue;

        /* internalize request context for the first instance only */
        if (!IS_PRIMARY_INSTANCE(module))
            continue;

        /* check for name match */
        if (strlen(module->name) != name->length ||
            memcmp(module->name, name->data, name->length) != 0)
            continue;

        ret = module;
        break;
    }

    return ret;
}

/*
 * In-place internalize authdata context, for modules that match given
 * flags mask. The magic identifier/trailer is not expected by this.
 */
static krb5_error_code
k5_ad_internalize(krb5_context kcontext,
                  krb5_authdata_context context,
                  krb5_flags flags,
                  krb5_octet **buffer,
                  size_t *lenremain)
{
    krb5_error_code code = 0;
    krb5_int32 i, count;
    krb5_octet *bp;
    size_t remain;

    bp = *buffer;
    remain = *lenremain;

    code = krb5_ser_unpack_int32(&count, &bp, &remain);
    if (code != 0)
        return code;

    for (i = 0; i < count; i++) {
        struct _krb5_authdata_context_module *module;
        krb5_int32 namelen;
        krb5_data name;

        code = krb5_ser_unpack_int32(&namelen, &bp, &remain);
        if (code != 0)
            break;

        if (remain < (size_t)namelen) {
            code = ENOMEM;
            break;
        }

        name.length = namelen;
        name.data = (char *)bp;

        module = k5_ad_find_module(kcontext, context, flags, &name);
        if (module == NULL || module->ftable->internalize == NULL) {
            code = EINVAL;
            break;
        }

        bp += namelen;
        remain -= namelen;

        code = (*module->ftable->internalize)(kcontext,
                                              context,
                                              module->plugin_context,
                                              *(module->request_context_pp),
                                              &bp,
                                              &remain);
        if (code != 0)
            break;
    }

    if (code == 0) {
        *buffer = bp;
        *lenremain = remain;
    }

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_context_init(krb5_context kcontext,
                           krb5_authdata_context *pcontext)
{
    int n_modules, n_tables, i, k;
    void **tables = NULL;
    krb5plugin_authdata_client_ftable_v0 *table;
    krb5_authdata_context context = NULL;
    int internal_count = 0;
    struct plugin_dir_handle plugins;
    krb5_error_code code;

    *pcontext = NULL;
    memset(&plugins, 0, sizeof(plugins));

    n_modules = 0;
    for (n_tables = 0; authdata_systems[n_tables] != NULL; n_tables++) {
        n_modules += k5_ad_module_count(authdata_systems[n_tables]);
    }
    internal_count = n_tables;

    if (PLUGIN_DIR_OPEN(&plugins) == 0 &&
        krb5int_open_plugin_dirs(objdirs, NULL,
                                 &plugins,
                                 &kcontext->err) == 0 &&
        krb5int_get_plugin_dir_data(&plugins,
                                    "authdata_client_0",
                                    &tables,
                                    &kcontext->err) == 0 &&
        tables != NULL)
    {
        for (; tables[n_tables - internal_count] != NULL; n_tables++) {
            table = tables[n_tables - internal_count];
            n_modules += k5_ad_module_count(table);
        }
    }

    context = calloc(1, sizeof(*context));
    if (kcontext == NULL) {
        code = ENOMEM;
        goto cleanup;
    }
    context->magic = KV5M_AUTHDATA_CONTEXT;
    context->modules = calloc(n_modules, sizeof(context->modules[0]));
    if (context->modules == NULL) {
        code = ENOMEM;
        goto cleanup;
    }
    context->n_modules = n_modules;

    /* fill in the structure */
    for (i = 0, k = 0, code = 0; i < n_tables - internal_count; i++) {
        code = k5_ad_init_modules(kcontext, context, tables[i], &k);
        if (code != 0)
            goto cleanup;
    }

    for (i = 0; i < internal_count; i++) {
        code = k5_ad_init_modules(kcontext, context, authdata_systems[i], &k);
        if (code != 0)
            goto cleanup;
    }

    context->plugins = plugins;

cleanup:
    if (tables != NULL)
        krb5int_free_plugin_dir_data(tables);

    if (code != 0) {
        krb5int_close_plugin_dirs(&plugins);
        krb5_authdata_context_free(kcontext, context);
    } else {
        /* plugins is owned by context now */
        *pcontext = context;
    }

    return code;
}

void KRB5_CALLCONV
krb5_authdata_context_free(krb5_context kcontext,
                           krb5_authdata_context context)
{
    int i;

    if (context == NULL)
        return;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];

        if (module->client_req_fini != NULL &&
            module->request_context != NULL)
            (*module->client_req_fini)(kcontext,
                                       context,
                                       module->plugin_context,
                                       module->request_context);

        if (module->client_fini != NULL)
            (*module->client_fini)(kcontext, module->plugin_context);

        memset(module, 0, sizeof(*module));
    }

    if (context->modules != NULL) {
        free(context->modules);
        context->modules = NULL;
    }
    krb5int_close_plugin_dirs(&context->plugins);
    memset(context, 0, sizeof(*context));
    free(context);
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_import_attributes(krb5_context kcontext,
                                krb5_authdata_context context,
                                krb5_flags usage,
                                const krb5_data *attrs)
{
    krb5_octet *bp;
    size_t remain;

    bp = (krb5_octet *)attrs->data;
    remain = attrs->length;

    return k5_ad_internalize(kcontext, context, usage, &bp, &remain);
}

static krb5_error_code
k5_get_kdc_issued_authdata(krb5_context kcontext,
                           const krb5_ap_req *ap_req,
                           krb5_principal *kdc_issuer,
                           krb5_authdata ***kdc_issued_authdata)
{
    krb5_error_code code;
    krb5_authdata **authdata;
    krb5_authdata **ticket_authdata;

    *kdc_issuer = NULL;
    *kdc_issued_authdata = NULL;

    ticket_authdata = ap_req->ticket->enc_part2->authorization_data;

    code = krb5_find_authdata(kcontext, ticket_authdata, NULL,
                              KRB5_AUTHDATA_KDC_ISSUED, &authdata);
    if (code != 0 || authdata == NULL)
        return code;

    /*
     * Note: a module must still implement a verify_authdata
     * method, even it is a NOOP that simply records the value
     * of the kdc_issued_flag.
     */
    code = krb5_verify_authdata_kdc_issued(kcontext,
                                           ap_req->ticket->enc_part2->session,
                                           authdata[0],
                                           kdc_issuer,
                                           kdc_issued_authdata);

    assert(code == 0 || *kdc_issued_authdata == NULL);

    krb5_free_authdata(kcontext, authdata);

    return code;
}

krb5_error_code
krb5int_authdata_verify(krb5_context kcontext,
                        krb5_authdata_context context,
                        krb5_flags usage,
                        const krb5_auth_context *auth_context,
                        const krb5_keyblock *key,
                        const krb5_ap_req *ap_req)
{
    int i;
    krb5_error_code code = 0;
    krb5_authdata **authen_authdata;
    krb5_authdata **ticket_authdata;
    krb5_principal kdc_issuer = NULL;
    krb5_authdata **kdc_issued_authdata = NULL;

    authen_authdata = (*auth_context)->authentp->authorization_data;
    ticket_authdata = ap_req->ticket->enc_part2->authorization_data;
    k5_get_kdc_issued_authdata(kcontext, ap_req,
                               &kdc_issuer, &kdc_issued_authdata);

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];
        krb5_authdata **authdata = NULL;
        krb5_boolean kdc_issued_flag = FALSE;

        if ((module->flags & usage) == 0)
            continue;

        if (module->ftable->import_authdata == NULL)
            continue;

        if (kdc_issued_authdata != NULL &&
            (module->flags & AD_USAGE_KDC_ISSUED)) {
            code = krb5_find_authdata(kcontext, kdc_issued_authdata, NULL,
                                      module->ad_type, &authdata);
            if (code != 0)
                break;

            kdc_issued_flag = TRUE;
        }

        if (authdata == NULL) {
            krb5_boolean ticket_usage = FALSE;
            krb5_boolean authen_usage = FALSE;

            /*
             * Determine which authdata sources to interrogate based on the
             * module's usage. This is important if the authdata is signed
             * by the KDC with the TGT key (as the user can forge that in
             * the AP-REQ).
             */
            if (module->flags & (AD_USAGE_AS_REQ | AD_USAGE_TGS_REQ))
                ticket_usage = TRUE;
            if (module->flags & AD_USAGE_AP_REQ)
                authen_usage = TRUE;

            code = krb5_find_authdata(kcontext,
                                      ticket_usage ? ticket_authdata : NULL,
                                      authen_usage ? authen_authdata : NULL,
                                      module->ad_type, &authdata);
            if (code != 0)
                break;
        }

        if (authdata == NULL)
            continue;

        assert(authdata[0] != NULL);

        code = (*module->ftable->import_authdata)(kcontext,
                                                  context,
                                                  module->plugin_context,
                                                  *(module->request_context_pp),
                                                  authdata,
                                                  kdc_issued_flag,
                                                  kdc_issuer);
        if (code == 0 && module->ftable->verify != NULL) {
            code = (*module->ftable->verify)(kcontext,
                                             context,
                                             module->plugin_context,
                                             *(module->request_context_pp),
                                             auth_context,
                                             key,
                                             ap_req);
        }
        if (code != 0 && (module->flags & AD_INFORMATIONAL))
            code = 0;
        krb5_free_authdata(kcontext, authdata);
        if (code != 0)
            break;
    }

    krb5_free_principal(kcontext, kdc_issuer);
    krb5_free_authdata(kcontext, kdc_issued_authdata);

    return code;
}

static krb5_error_code
k5_merge_data_list(krb5_data **dst, krb5_data *src, unsigned int *len)
{
    unsigned int i;
    krb5_data *d;

    if (src == NULL)
        return 0;

    for (i = 0; src[i].data != NULL; i++)
        ;

    d = realloc(*dst, (*len + i + 1) * sizeof(krb5_data));
    if (d == NULL)
        return ENOMEM;

    memcpy(&d[*len], src, i * sizeof(krb5_data));

    *len += i;

    d[*len].data = NULL;
    d[*len].length = 0;

    *dst = d;

    return 0;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_get_attribute_types(krb5_context kcontext,
                                  krb5_authdata_context context,
                                  krb5_data **out_attrs)
{
    int i;
    krb5_error_code code = 0;
    krb5_data *attrs = NULL;
    unsigned int attrs_len = 0;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];
        krb5_data *attrs2 = NULL;

        if (module->ftable->get_attribute_types == NULL)
            continue;

        if ((*module->ftable->get_attribute_types)(kcontext,
                                                   context,
                                                   module->plugin_context,
                                                   *(module->request_context_pp),
                                                   &attrs2))
            continue;

        code = k5_merge_data_list(&attrs, attrs2, &attrs_len);
        if (code != 0) {
            krb5int_free_data_list(kcontext, attrs2);
            break;
        }
        if (attrs2 != NULL)
            free(attrs2);
    }

    if (code != 0) {
        krb5int_free_data_list(kcontext, attrs);
        attrs = NULL;
    }

    *out_attrs = attrs;

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_get_attribute(krb5_context kcontext,
                            krb5_authdata_context context,
                            const krb5_data *attribute,
                            krb5_boolean *authenticated,
                            krb5_boolean *complete,
                            krb5_data *value,
                            krb5_data *display_value,
                            int *more)
{
    int i;
    krb5_error_code code = ENOENT;

    *authenticated = FALSE;
    *complete = FALSE;

    value->data = NULL;
    value->length = 0;

    display_value->data = NULL;
    display_value->length = 0;

    /*
     * NB at present a module is presumed to be authoritative for
     * an attribute; not sure how to federate "more" across module
     * yet
     */
    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];

        if (module->ftable->get_attribute == NULL)
            continue;

        code = (*module->ftable->get_attribute)(kcontext,
                                                context,
                                                module->plugin_context,
                                                *(module->request_context_pp),
                                                attribute,
                                                authenticated,
                                                complete,
                                                value,
                                                display_value,
                                                more);
        if (code == 0)
            break;
    }

    if (code != 0)
        *more = 0;

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_set_attribute(krb5_context kcontext,
                            krb5_authdata_context context,
                            krb5_boolean complete,
                            const krb5_data *attribute,
                            const krb5_data *value)
{
    int i;
    krb5_error_code code = 0;
    int found = 0;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];

        if (module->ftable->set_attribute == NULL)
            continue;

        code = (*module->ftable->set_attribute)(kcontext,
                                                context,
                                                module->plugin_context,
                                                *(module->request_context_pp),
                                                complete,
                                                attribute,
                                                value);
        if (code == ENOENT)
            code = 0;
        else if (code == 0)
            found++;
        else
            break;
    }

    if (code == 0 && found == 0)
        code = ENOENT;

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_delete_attribute(krb5_context kcontext,
                               krb5_authdata_context context,
                               const krb5_data *attribute)
{
    int i;
    krb5_error_code code = ENOENT;
    int found = 0;

    for (i = 0; i < context->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &context->modules[i];

        if (module->ftable->delete_attribute == NULL)
            continue;

        code = (*module->ftable->delete_attribute)(kcontext,
                                                   context,
                                                   module->plugin_context,
                                                   *(module->request_context_pp),
                                                   attribute);
        if (code == ENOENT)
            code = 0;
        else if (code == 0)
            found++;
        else
            break;
    }

    if (code == 0 && found == 0)
        code = ENOENT;

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_export_attributes(krb5_context kcontext,
                                krb5_authdata_context context,
                                krb5_flags flags,
                                krb5_data **attrsp)
{
    krb5_error_code code;
    size_t required = 0;
    krb5_octet *bp;
    size_t remain;
    krb5_data *attrs;

    code = k5_ad_size(kcontext, context, AD_USAGE_MASK, &required);
    if (code != 0)
        return code;

    attrs = malloc(sizeof(*attrs));
    if (attrs == NULL)
        return ENOMEM;

    attrs->magic = KV5M_DATA;
    attrs->length = 0;
    attrs->data = malloc(required);
    if (attrs->data == NULL) {
        free(attrs);
        return ENOMEM;
    }

    bp = (krb5_octet *)attrs->data;
    remain = required;

    code = k5_ad_externalize(kcontext, context, AD_USAGE_MASK, &bp, &remain);
    if (code != 0) {
        krb5_free_data(kcontext, attrs);
        return code;
    }

    attrs->length = (bp - (krb5_octet *)attrs->data);

    *attrsp = attrs;

    return 0;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_export_internal(krb5_context kcontext,
                              krb5_authdata_context context,
                              krb5_boolean restrict_authenticated,
                              const char *module_name,
                              void **ptr)
{
    krb5_error_code code;
    krb5_data name;
    struct _krb5_authdata_context_module *module;

    *ptr = NULL;

    name.length = strlen(module_name);
    name.data = (char *)module_name;

    module = k5_ad_find_module(kcontext, context, AD_USAGE_MASK, &name);
    if (module == NULL)
        return ENOENT;

    if (module->ftable->export_internal == NULL)
        return ENOENT;

    code = (*module->ftable->export_internal)(kcontext,
                                              context,
                                              module->plugin_context,
                                              *(module->request_context_pp),
                                              restrict_authenticated,
                                              ptr);

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_free_internal(krb5_context kcontext,
                            krb5_authdata_context context,
                            const char *module_name,
                            void *ptr)
{
    krb5_data name;
    struct _krb5_authdata_context_module *module;

    name.length = strlen(module_name);
    name.data = (char *)module_name;

    module = k5_ad_find_module(kcontext, context, AD_USAGE_MASK, &name);
    if (module == NULL)
        return ENOENT;

    if (module->ftable->free_internal == NULL)
        return ENOENT;

    (*module->ftable->free_internal)(kcontext,
                                     context,
                                     module->plugin_context,
                                     *(module->request_context_pp),
                                     ptr);

    return 0;
}

static krb5_error_code
k5_copy_ad_module_data(krb5_context kcontext,
                       krb5_authdata_context context,
                       struct _krb5_authdata_context_module *src_module,
                       krb5_authdata_context dst)
{
    int i;
    krb5_error_code code;
    struct _krb5_authdata_context_module *dst_module = NULL;

    for (i = 0; i < dst->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &dst->modules[i];

        if (module->ftable == src_module->ftable) {
            /* XXX is this safe to assume these pointers are interned? */
            dst_module = module;
            break;
        }
    }

    if (dst_module == NULL)
        return ENOENT;

    /* copy request context for the first instance only */
    if (!IS_PRIMARY_INSTANCE(dst_module))
        return 0;

    assert(strcmp(dst_module->name, src_module->name) == 0);

    /* If copy is unimplemented, externalize/internalize */
    if (src_module->ftable->copy == NULL) {
        size_t size = 0, remain;
        krb5_octet *contents, *bp;

        assert(src_module->ftable->size != NULL);
        assert(src_module->ftable->externalize != NULL);
        assert(dst_module->ftable->internalize != NULL);

        code = (*src_module->ftable->size)(kcontext,
                                           context,
                                           src_module->plugin_context,
                                           src_module->request_context,
                                           &size);
        if (code != 0)
            return code;

        contents = malloc(size);
        if (contents == NULL)
            return ENOMEM;

        bp = contents;
        remain = size;

        code = (*src_module->ftable->externalize)(kcontext,
                                                  context,
                                                  src_module->plugin_context,
                                                  *(src_module->request_context_pp),
                                                  &bp,
                                                  &remain);
        if (code != 0) {
            free(contents);
            return code;
        }

        remain = (bp - contents);
        bp = contents;

        code = (*dst_module->ftable->internalize)(kcontext,
                                                  context,
                                                  dst_module->plugin_context,
                                                  *(dst_module->request_context_pp),
                                                  &bp,
                                                  &remain);
        if (code != 0) {
            free(contents);
            return code;
        }

        free(contents);
    } else {
        assert(src_module->request_context_pp == &src_module->request_context);
        assert(dst_module->request_context_pp == &dst_module->request_context);

        code = (*src_module->ftable->copy)(kcontext,
                                           context,
                                           src_module->plugin_context,
                                           src_module->request_context,
                                           dst_module->plugin_context,
                                           dst_module->request_context);
    }

    return code;
}

krb5_error_code KRB5_CALLCONV
krb5_authdata_context_copy(krb5_context kcontext,
                           krb5_authdata_context src,
                           krb5_authdata_context *pdst)
{
    int i;
    krb5_error_code code;
    krb5_authdata_context dst;

    /* XXX we need to init a new context because we can't copy plugins */
    code = krb5_authdata_context_init(kcontext, &dst);
    if (code != 0)
        return code;

    for (i = 0; i < src->n_modules; i++) {
        struct _krb5_authdata_context_module *module = &src->modules[i];

        code = k5_copy_ad_module_data(kcontext, src, module, dst);
        if (code != 0)
            break;
    }

    if (code != 0) {
        krb5_authdata_context_free(kcontext, dst);
        return code;
    }

    *pdst = dst;

    return 0;
}

/*
 * Calculate size of to-be-externalized authdata context.
 */
static krb5_error_code
krb5_authdata_context_size(krb5_context kcontext,
                           krb5_pointer ptr,
                           size_t *sizep)
{
    krb5_error_code code;
    krb5_authdata_context context = (krb5_authdata_context)ptr;

    code = k5_ad_size(kcontext, context, AD_USAGE_MASK, sizep);
    if (code != 0)
        return code;

    *sizep += 2 * sizeof(krb5_int32); /* identifier/trailer */

    return 0;
}

/*
 * Externalize an authdata context.
 */
static krb5_error_code
krb5_authdata_context_externalize(krb5_context kcontext,
                                  krb5_pointer ptr,
                                  krb5_octet **buffer,
                                  size_t *lenremain)
{
    krb5_error_code code;
    krb5_authdata_context context = (krb5_authdata_context)ptr;
    krb5_octet *bp;
    size_t remain;

    bp = *buffer;
    remain = *lenremain;

    /* Our identifier */
    code = krb5_ser_pack_int32(KV5M_AUTHDATA_CONTEXT, &bp, &remain);
    if (code != 0)
        return code;

    /* The actual context data */
    code = k5_ad_externalize(kcontext, context, AD_USAGE_MASK,
                             &bp, &remain);
    if (code != 0)
        return code;

    /* Our trailer */
    code = krb5_ser_pack_int32(KV5M_AUTHDATA_CONTEXT, &bp, &remain);
    if (code != 0)
        return code;

    *buffer = bp;
    *lenremain = remain;

    return 0;
}

/*
 * Internalize an authdata context.
 */
static krb5_error_code
krb5_authdata_context_internalize(krb5_context kcontext,
                                  krb5_pointer *ptr,
                                  krb5_octet **buffer,
                                  size_t *lenremain)
{
    krb5_error_code code;
    krb5_authdata_context context;
    krb5_int32 ibuf;
    krb5_octet *bp;
    size_t remain;

    bp = *buffer;
    remain = *lenremain;

    code = krb5_ser_unpack_int32(&ibuf, &bp, &remain);
    if (code != 0)
        return code;

    if (ibuf != KV5M_AUTHDATA_CONTEXT)
        return EINVAL;

    code = krb5_authdata_context_init(kcontext, &context);
    if (code != 0)
        return code;

    code = k5_ad_internalize(kcontext, context, AD_USAGE_MASK,
                             &bp, &remain);
    if (code != 0) {
        krb5_authdata_context_free(kcontext, context);
        return code;
    }

    code = krb5_ser_unpack_int32(&ibuf, &bp, &remain);
    if (code != 0)
        return code;

    if (ibuf != KV5M_AUTHDATA_CONTEXT) {
        krb5_authdata_context_free(kcontext, context);
        return EINVAL;
    }

    *buffer = bp;
    *lenremain = remain;
    *ptr = context;

    return 0;
}

static const krb5_ser_entry krb5_authdata_context_ser_entry = {
    KV5M_AUTHDATA_CONTEXT,
    krb5_authdata_context_size,
    krb5_authdata_context_externalize,
    krb5_authdata_context_internalize
};

/*
 * Register the authdata context serializer.
 */
krb5_error_code
krb5_ser_authdata_context_init(krb5_context kcontext)
{
    return krb5_register_serializer(kcontext,
                                    &krb5_authdata_context_ser_entry);
}

krb5_error_code
krb5int_copy_authdatum(krb5_context context,
                       const krb5_authdata *inad, krb5_authdata **outad)
{
    krb5_authdata *tmpad;

    if (!(tmpad = (krb5_authdata *)malloc(sizeof(*tmpad))))
        return ENOMEM;
    *tmpad = *inad;
    if (!(tmpad->contents = (krb5_octet *)malloc(inad->length))) {
        free(tmpad);
        return ENOMEM;
    }
    memcpy(tmpad->contents, inad->contents, inad->length);
    *outad = tmpad;
    return 0;
}

void KRB5_CALLCONV
krb5_free_authdata(krb5_context context, krb5_authdata **val)
{
    register krb5_authdata **temp;

    if (val == NULL)
        return;
    for (temp = val; *temp; temp++) {
        free((*temp)->contents);
        free(*temp);
    }
    free(val);
}
