/*
 * Kerberos related utils for FreeIPA
 *
 * Authors: Simo Sorce <ssorce@redhat.com>
 *
 * Copyright (C) 2011  Simo Sorce, Red Hat
 * see file 'COPYING' for use and warranty information
 *
 * This program is free software you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <lber.h>
#include <errno.h>

#include <libintl.h>
#define _(STRING) gettext(STRING)

#include "ipa_krb5.h"

#define TOSTR(x) STR(x)
#define STR(x) #x
const char *ipapwd_password_max_len_errmsg = \
    "clear-text password is too long (max " \
    TOSTR(IPAPWD_PASSWORD_MAX_LEN) \
    " chars)!";

/* Salt types */
#define KRB5P_SALT_SIZE 16

static krb5_error_code ipa_get_random_salt(krb5_context krbctx,
                                           krb5_data *salt)
{
    krb5_error_code kerr;
    int i, v;

    /* make random salt */
    salt->length = KRB5P_SALT_SIZE;
    salt->data = malloc(KRB5P_SALT_SIZE);
    if (!salt->data) {
        return ENOMEM;
    }
    kerr = krb5_c_random_make_octets(krbctx, salt);
    if (kerr) {
        return kerr;
    }

    /* Windows treats the salt as a string.
     * To avoid any compatibility issue, limits octects only to
     * the ASCII printable range, or 0x20 <= val <= 0x7E */
    for (i = 0; i < salt->length; i++) {
        v = (unsigned char)salt->data[i];
        v %= 0x5E; /* 7E - 20 */
        v += 0x20; /* add base */
        salt->data[i] = v;
    }

    return 0;
}

void
ipa_krb5_free_ktypes(krb5_context context, krb5_enctype *val)
{
    free(val);
}

/*
 * Convert a krb5_principal into the default salt for that principal.
 */
krb5_error_code ipa_krb5_principal2salt_norealm(krb5_context context,
                                                krb5_const_principal pr,
                                                krb5_data *ret)
{
    unsigned int size = 0, offset=0;
    krb5_int32 nelem;
    register int i;

    if (pr == NULL) {
        ret->length = 0;
        ret->data = NULL;
        return 0;
    }

    nelem = krb5_princ_size(context, pr);

    for (i = 0; i < (int) nelem; i++)
        size += krb5_princ_component(context, pr, i)->length;

    ret->length = size;
    if (!(ret->data = malloc (size)))
        return ENOMEM;

    for (i = 0; i < (int) nelem; i++) {
        memcpy(&ret->data[offset], krb5_princ_component(context, pr, i)->data,
               krb5_princ_component(context, pr, i)->length);
        offset += krb5_princ_component(context, pr, i)->length;
    }
    return 0;
}

void krb5int_c_free_keyblock_contents(krb5_context context,
                                      register krb5_keyblock *key);

/*
 * Generate a krb5_key_data set by encrypting keys according to
 * enctype/salttype preferences
 */
krb5_error_code ipa_krb5_generate_key_data(krb5_context krbctx,
                                           krb5_principal principal,
                                           krb5_data pwd, int kvno,
                                           krb5_keyblock *kmkey,
                                           int num_encsalts,
                                           krb5_key_salt_tuple *encsalts,
                                           int *_num_keys,
                                           krb5_key_data **_keys)
{
    krb5_error_code kerr;
    krb5_key_data *keys;
    int num_keys;
    int i;

    if ((pwd.data != NULL) && (pwd.length > IPAPWD_PASSWORD_MAX_LEN)) {
        kerr = E2BIG;
        krb5_set_error_message(krbctx, kerr, "%s",
                               ipapwd_password_max_len_errmsg);
        return kerr;
    }

    num_keys = num_encsalts;
    keys = calloc(num_keys, sizeof(krb5_key_data));
    if (!keys) {
        return ENOMEM;
    }

    for (i = 0; i < num_keys; i++) {
        krb5_keyblock key;
        krb5_data salt;
        krb5_octet *ptr;
        krb5_data plain;
        krb5_enc_data cipher;
        krb5_int16 t;
        size_t len;

        salt.data = NULL;

        keys[i].key_data_ver = 2; /* we always have a salt */
        keys[i].key_data_kvno = kvno;

        switch (encsalts[i].ks_salttype) {

        case KRB5_KDB_SALTTYPE_ONLYREALM:

            if (!principal->realm.data) {
                kerr = EINVAL;
                goto done;
            }
            salt.length = principal->realm.length;
            salt.data = malloc(salt.length);
            if (!salt.data) {
                kerr = ENOMEM;
                goto done;
            }
            memcpy(salt.data, principal->realm.data, salt.length);
            break;

        case KRB5_KDB_SALTTYPE_NOREALM:

            kerr = ipa_krb5_principal2salt_norealm(krbctx, principal, &salt);
            if (kerr) {
                goto done;
            }
            break;

        case KRB5_KDB_SALTTYPE_NORMAL:

            kerr = krb5_principal2salt(krbctx, principal, &salt);
            if (kerr) {
                goto done;
            }
            break;

        case KRB5_KDB_SALTTYPE_SPECIAL:

            kerr = ipa_get_random_salt(krbctx, &salt);
            if (kerr) {
                goto done;
            }
            break;

        case KRB5_KDB_SALTTYPE_V4:
            salt.length = 0;
            break;

        case KRB5_KDB_SALTTYPE_AFS3:

            if (!principal->realm.data) {
                kerr = EINVAL;
                goto done;
            }
            salt.data = strndup((char *)principal->realm.data,
                                        principal->realm.length);
            if (!salt.data) {
                kerr = ENOMEM;
                goto done;
            }
            salt.length = SALT_TYPE_AFS_LENGTH; /* special value */
            break;

        default:
            kerr = EINVAL;
            goto done;
        }

        /* need to build the key now to manage the AFS salt.length
         * special case */
        if (pwd.data == NULL) {
            kerr = krb5_c_make_random_key(krbctx,
                                          encsalts[i].ks_enctype,
                                          &key);
        } else {
            kerr = krb5_c_string_to_key(krbctx,
                                        encsalts[i].ks_enctype,
                                        &pwd, &salt, &key);
        }
        if (kerr) {
            krb5_free_data_contents(krbctx, &salt);
            goto done;
        }
        if (salt.length == SALT_TYPE_AFS_LENGTH) {
            salt.length = strlen(salt.data);
        }

        kerr = krb5_c_encrypt_length(krbctx,
                                     kmkey->enctype, key.length, &len);
        if (kerr) {
            krb5int_c_free_keyblock_contents(krbctx, &key);
            krb5_free_data_contents(krbctx, &salt);
            goto done;
        }

        if ((ptr = (krb5_octet *) malloc(2 + len)) == NULL) {
            kerr = ENOMEM;
            krb5int_c_free_keyblock_contents(krbctx, &key);
            krb5_free_data_contents(krbctx, &salt);
            goto done;
        }

        t = htole16(key.length);
        memcpy(ptr, &t, 2);

        plain.length = key.length;
        plain.data = (char *)key.contents;

        cipher.ciphertext.length = len;
        cipher.ciphertext.data = (char *)ptr+2;

        kerr = krb5_c_encrypt(krbctx, kmkey, 0, 0, &plain, &cipher);
        if (kerr) {
            krb5int_c_free_keyblock_contents(krbctx, &key);
            krb5_free_data_contents(krbctx, &salt);
            free(ptr);
            goto done;
        }

        /* KrbSalt  */
        keys[i].key_data_type[1] = encsalts[i].ks_salttype;

        if (salt.length) {
            keys[i].key_data_length[1] = salt.length;
            keys[i].key_data_contents[1] = (krb5_octet *)salt.data;
        }

        /* EncryptionKey */
        keys[i].key_data_type[0] = key.enctype;
        keys[i].key_data_length[0] = len + 2;
        keys[i].key_data_contents[0] = malloc(len + 2);
        if (!keys[i].key_data_contents[0]) {
            kerr = ENOMEM;
            krb5int_c_free_keyblock_contents(krbctx, &key);
            free(ptr);
            goto done;
        }
        memcpy(keys[i].key_data_contents[0], ptr, len + 2);

        /* make sure we free the memory used now that we are done with it */
        krb5int_c_free_keyblock_contents(krbctx, &key);
        free(ptr);
    }

    *_num_keys = num_keys;
    *_keys = keys;
    kerr = 0;

done:
    if (kerr) {
        ipa_krb5_free_key_data(keys, num_keys);
    }

    return kerr;
}

void ipa_krb5_free_key_data(krb5_key_data *keys, int num_keys)
{
    int i;

    if (keys == NULL)
        return;

    for (i = 0; i < num_keys; i++) {
        /* try to wipe key from memory,
         * hopefully the compiler will not optimize it away */
        if (keys[i].key_data_length[0]) {
            memset(keys[i].key_data_contents[0],
                   0, keys[i].key_data_length[0]);
        }
        free(keys[i].key_data_contents[0]);
        free(keys[i].key_data_contents[1]);
    }
    free(keys);
}

/* Novell key-format scheme:

   KrbKeySet ::= SEQUENCE {
   attribute-major-vno       [0] UInt16,
   attribute-minor-vno       [1] UInt16,
   kvno                      [2] UInt32,
   mkvno                     [3] UInt32 OPTIONAL,
   keys                      [4] SEQUENCE OF KrbKey,
   ...
   }

   KrbKey ::= SEQUENCE {
   salt      [0] KrbSalt OPTIONAL,
   key       [1] EncryptionKey,
   s2kparams [2] OCTET STRING OPTIONAL,
    ...
   }

   KrbSalt ::= SEQUENCE {
   type      [0] Int32,
   salt      [1] OCTET STRING OPTIONAL
   }

   EncryptionKey ::= SEQUENCE {
   keytype   [0] Int32,
   keyvalue  [1] OCTET STRING
   }

 */

int ber_encode_krb5_key_data(krb5_key_data *data,
                             int numk, int mkvno,
                             struct berval **encoded)
{
    BerElement *be = NULL;
    ber_tag_t tag;
    int ret, i;

    be = ber_alloc_t(LBER_USE_DER);
    if (!be) {
        return ENOMEM;
    }

    tag = LBER_CONSTRUCTED | LBER_CLASS_CONTEXT;

    ret = ber_printf(be, "{t[i]t[i]t[i]t[i]t[{",
                         tag | 0, 1, tag | 1, 1,
                         tag | 2, (ber_int_t)data[0].key_data_kvno,
                         tag | 3, (ber_int_t)mkvno, tag | 4);
    if (ret == -1) {
        ret = EFAULT;
        goto done;
    }

    for (i = 0; i < numk; i++) {

        ret = ber_printf(be, "{");
        if (ret == -1) {
            ret = EFAULT;
            goto done;
        }

        if (data[i].key_data_length[1] != 0) {
            ret = ber_printf(be, "t[{t[i]",
                                 tag | 0,
                                   tag | 0,
                                     (ber_int_t)data[i].key_data_type[1]);
            if (ret != -1) {
                ret = ber_printf(be, "t[o]",
                                     tag | 1,
                                       data[i].key_data_contents[1],
                                       (ber_len_t)data[i].key_data_length[1]);
            }
            if (ret != -1) {
                ret = ber_printf(be, "}]");
            }
            if (ret == -1) {
                ret = EFAULT;
                goto done;
            }
        }

        ret = ber_printf(be, "t[{t[i]t[o]}]",
                              tag | 1,
                                tag | 0,
                                  (ber_int_t)data[i].key_data_type[0],
                                tag | 1,
                                  data[i].key_data_contents[0],
                                  (ber_len_t)data[i].key_data_length[0]);
        if (ret == -1) {
            ret = EFAULT;
            goto done;
        }

        ret = ber_printf(be, "}");
        if (ret == -1) {
            ret = EFAULT;
            goto done;
        }
    }

    ret = ber_printf(be, "}]}");
    if (ret == -1) {
        ret = EFAULT;
        goto done;
    }

    ret = ber_flatten(be, encoded);
    if (ret == -1) {
        ret = EFAULT;
        goto done;
    }

done:
    ber_free(be, 1);
    return ret;
}

int ber_decode_krb5_key_data(struct berval *encoded, int *m_kvno,
                             int *numk, krb5_key_data **data)
{
    krb5_key_data *keys = NULL;
    BerElement *be = NULL;
    void *tmp;
    int i = 0;
    ber_tag_t tag;
    ber_int_t major_vno;
    ber_int_t minor_vno;
    ber_int_t kvno;
    ber_int_t mkvno;
    ber_int_t type;
    ber_tag_t seqtag;
    ber_len_t seqlen;
    ber_len_t setlen;
    ber_tag_t retag;
    ber_tag_t opttag;
    struct berval tval;
    int ret;

    be = ber_alloc_t(LBER_USE_DER);
    if (!be) {
        return ENOMEM;
    }

    /* reinit the ber element with the new val */
    ber_init2(be, encoded, LBER_USE_DER);

    /* fill key_data struct with the data */
    retag = ber_scanf(be, "{t[i]t[i]t[i]t[i]t[{",
                      &tag, &major_vno,
                      &tag, &minor_vno,
                      &tag, &kvno,
                      &tag, &mkvno,
                      &seqtag);
    if (retag == LBER_ERROR ||
            major_vno != 1 ||
            minor_vno != 1 ||
            seqtag != (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 4)) {
        ret = EINVAL;
        goto done;
    }

    retag = ber_skip_tag(be, &seqlen);

    /* sequence of keys */
    for (i = 0; retag == LBER_SEQUENCE; i++) {

        tmp = realloc(keys, (i + 1) * sizeof(krb5_key_data));
        if (!tmp) {
            ret = ENOMEM;
            goto done;
        }
        keys = tmp;

        memset(&keys[i], 0, sizeof(krb5_key_data));

        keys[i].key_data_kvno = kvno;

        /* do we have a salt type ? (optional) */
        retag = ber_scanf(be, "t", &opttag);
        if (retag == LBER_ERROR) {
            ret = EINVAL;
            goto done;
        }
        if (opttag == (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0)) {
            keys[i].key_data_ver = 2;

            retag = ber_scanf(be, "[l{tl[i]",
                              &seqlen, &tag, &setlen, &type);
            if (tag != (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0)) {
                ret = EINVAL;
                goto done;
            }
            keys[i].key_data_type[1] = type;

            /* do we have salt data ? (optional) */
            if (seqlen > setlen + 2) {
                retag = ber_scanf(be, "t[o]", &tag, &tval);
                if (retag == LBER_ERROR ||
                    tag != (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1)) {
                    ret = EINVAL;
                    goto done;
                }
                keys[i].key_data_length[1] = tval.bv_len;
                keys[i].key_data_contents[1] = (krb5_octet *)tval.bv_val;
            }

            retag = ber_scanf(be, "}]t", &opttag);
            if (retag == LBER_ERROR) {
                ret = EINVAL;
                goto done;
            }

        } else {
            keys[i].key_data_ver = 1;
        }

        if (opttag != (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1)) {
            ret = EINVAL;
            goto done;
        }

        /* get the key */
        retag = ber_scanf(be, "[{t[i]t[o]}]", &tag, &type, &tag, &tval);
        if (retag == LBER_ERROR) {
            ret = EINVAL;
            goto done;
        }
        keys[i].key_data_type[0] = type;
        keys[i].key_data_length[0] = tval.bv_len;
        keys[i].key_data_contents[0] = (krb5_octet *)tval.bv_val;

        /* check for sk2params */
        retag = ber_peek_tag(be, &setlen);
        if (retag == (LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 2)) {
            /* not supported yet, skip */
            retag = ber_scanf(be, "t[x]}", &tag);
        } else {
            retag = ber_scanf(be, "}");
        }
        if (retag == LBER_ERROR) {
            ret = EINVAL;
            goto done;
        }

        retag = ber_skip_tag(be, &seqlen);
    }

    ret = 0;

done:
    ber_free(be, 0); /* internal buffer is 'encoded' */
    if (ret) {
        for (i -= 1; keys && i >= 0; i--) {
            free(keys[i].key_data_contents[0]);
            free(keys[i].key_data_contents[1]);
        }
        free(keys);
        keys = NULL;
        mkvno = 0;
    }
    *m_kvno = mkvno;
    *numk = i;
    *data = keys;
    return ret;
}


krb5_error_code parse_bval_key_salt_tuples(krb5_context kcontext,
                                           const char * const *vals,
                                           int n_vals,
                                           krb5_key_salt_tuple **kst,
                                           int *n_kst)
{
    krb5_error_code kerr;
    krb5_key_salt_tuple *ks;
    int n_ks;
    int i;

    ks = calloc(n_vals + 1, sizeof(krb5_key_salt_tuple));
    if (!ks) {
        return ENOMEM;
    }

    for (i = 0, n_ks = 0; i < n_vals; i++) {
        char *enc, *salt;
        krb5_int32 tmpsalt;
        krb5_enctype tmpenc;
        krb5_boolean similar;
        krb5_error_code krberr;
        int j;

        enc = strdup(vals[i]);
        if (!enc) {
            kerr = ENOMEM;
            goto fail;
        }

        salt = strchr(enc, ':');
        if (!salt) {
            free(enc);
            continue;
        }
        *salt = '\0'; /* null terminate the enc type */
        salt++; /* skip : */

        krberr = krb5_string_to_enctype(enc, &tmpenc);
        if (krberr) {
            free(enc);
            continue;
        }

        krberr = krb5_string_to_salttype(salt, &tmpsalt);
        for (j = 0; j < n_ks; j++) {
            krb5_c_enctype_compare(kcontext,
                                   ks[j].ks_enctype, tmpenc, &similar);
            if (similar && (ks[j].ks_salttype == tmpsalt)) {
                break;
            }
        }

        if (j == n_ks) {
            /* not found */
            ks[j].ks_enctype = tmpenc;
            ks[j].ks_salttype = tmpsalt;
            n_ks++;
        }

        free(enc);
    }

    *kst = ks;
    *n_kst = n_ks;

    return 0;

fail:
    free(ks);
    return kerr;
}

krb5_error_code filter_key_salt_tuples(krb5_context context,
                                       krb5_key_salt_tuple *req, int n_req,
                                       krb5_key_salt_tuple *supp, int n_supp,
                                       krb5_key_salt_tuple **res, int *n_res)
{
    krb5_key_salt_tuple *ks = NULL;
    int n_ks;
    int i, j;

    ks = calloc(n_req, sizeof(krb5_key_salt_tuple));
    if (!ks) {
        return ENOMEM;
    }
    n_ks = 0;

    for (i = 0; i < n_req; i++) {
        for (j = 0; j < n_supp; j++) {
            if (req[i].ks_enctype == supp[j].ks_enctype &&
                req[i].ks_salttype == supp[j].ks_salttype) {
                break;
            }
        }
        if (j < n_supp) {
            ks[n_ks] = req[i];
            n_ks++;
        }
    }

    *res = ks;
    *n_res = n_ks;
    return 0;
}

struct berval *create_key_control(struct keys_container *keys,
                                  const char *principalName)
{
    struct krb_key_salt *ksdata;
    struct berval *bval;
    BerElement *be;
    int ret, i;

    be = ber_alloc_t(LBER_USE_DER);
    if (!be) {
        return NULL;
    }

    ret = ber_printf(be, "{s{", principalName);
    if (ret == -1) {
        ber_free(be, 1);
        return NULL;
    }

    ksdata = keys->ksdata;
    for (i = 0; i < keys->nkeys; i++) {

        /* we set only the EncryptionKey and salt, no s2kparams */

        ret = ber_printf(be, "{t[{t[i]t[o]}]",
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
                 (ber_int_t)ksdata[i].enctype,
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
                 (char *)ksdata[i].key.contents, (ber_len_t)ksdata[i].key.length);

        if (ret == -1) {
            ber_free(be, 1);
            return NULL;
        }

        if (ksdata[i].salttype == NO_SALT) {
            ret = ber_printf(be, "}");
            if (ret == -1) {
                ber_free(be, 1);
                return NULL;
            }
            continue;
        }

        /* we have to pass a salt structure */
        ret = ber_printf(be, "t[{t[i]t[o]}]}",
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
                 (ber_int_t)ksdata[i].salttype,
                 (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
                 (char *)ksdata[i].salt.data, (ber_len_t)ksdata[i].salt.length);

        if (ret == -1) {
            ber_free(be, 1);
            return NULL;
        }
    }

    ret = ber_printf(be, "}}");
    if (ret == -1) {
        ber_free(be, 1);
        return NULL;
    }

    ret = ber_flatten(be, &bval);
    if (ret == -1) {
        ber_free(be, 1);
        return NULL;
    }

    ber_free(be, 1);
    return bval;
}

void free_keys_contents(krb5_context krbctx, struct keys_container *keys)
{
    struct krb_key_salt *ksdata;
    int i;

    ksdata = keys->ksdata;
    for (i = 0; i < keys->nkeys; i++) {
        krb5_free_keyblock_contents(krbctx, &ksdata[i].key);
        krb5_free_data_contents(krbctx, &ksdata[i].salt);
    }
    free(ksdata);

    keys->ksdata = NULL;
    keys->nkeys = 0;
}

int ipa_string_to_enctypes(const char *str, struct krb_key_salt **encsalts,
                           int *num_encsalts, char **err_msg)
{
    struct krb_key_salt *ksdata;
    krb5_error_code krberr;
    char *tmp, *t;
    int count;
    int num;

    *err_msg = NULL;

    tmp = strdup(str);
    if (!tmp) {
        *err_msg = _("Out of memory\n");
        return ENOMEM;
    }

    /* count */
    count = 0;
    for (t = tmp; t; t = strchr(t, ',')) {
        count++;
        t++;
    }
    count++; /* count the last one that is 0 terminated instead */

    /* at the end we will have at most count entries + 1 terminating */
    ksdata = calloc(count + 1, sizeof(struct krb_key_salt));
    if (!ksdata) {
        *err_msg = _("Out of memory\n");
        free(tmp);
        return ENOMEM;
    }

    num = 0;
    t = tmp;
    for (int i = 0; i < count; i++) {
        char *p, *q;

        p = strchr(t, ',');
        if (p) *p = '\0';

        q = strchr(t, ':');
        if (q) *q++ = '\0';

        krberr = krb5_string_to_enctype(t, &ksdata[num].enctype);
        if (krberr) {
            *err_msg = _("Warning unrecognized encryption type.\n");
            if (p) t = p + 1;
            continue;
        }
        if (p) t = p + 1;

        if (!q) {
            ksdata[num].salttype = KRB5_KDB_SALTTYPE_NORMAL;
            num++;
            continue;
        }

        krberr = krb5_string_to_salttype(q, &ksdata[num].salttype);
        if (krberr) {
            *err_msg = _("Warning unrecognized salt type.\n");
            continue;
        }

        num++;
    }

    *num_encsalts = num;
    *encsalts = ksdata;
    free(tmp);
    return 0;
}

/* Determines Encryption and Salt types,
 * allocates key_salt data storage,
 * filters out equivalent encodings,
 * returns 0 if no enctypes available, >0 if enctypes are available */
static int prep_ksdata(krb5_context krbctx, const char *str,
                       struct keys_container *keys,
                       char **err_msg)
{
    struct krb_key_salt *ksdata;
    krb5_error_code krberr;
    int n, i, j, nkeys;

    *err_msg = NULL;

    if (str == NULL) {
        krb5_enctype *ktypes;

        krberr = krb5_get_permitted_enctypes(krbctx, &ktypes);
        if (krberr) {
            *err_msg = _("No system preferred enctypes ?!\n");
            return 0;
        }

        for (n = 0; ktypes[n]; n++) /* count */ ;

        ksdata = calloc(n + 1, sizeof(struct krb_key_salt));
        if (NULL == ksdata) {
            *err_msg = _("Out of memory!?\n");
            return 0;
        }

        for (i = 0; i < n; i++) {
            ksdata[i].enctype = ktypes[i];
            ksdata[i].salttype = KRB5_KDB_SALTTYPE_NORMAL;
        }

        ipa_krb5_free_ktypes(krbctx, ktypes);

        nkeys = i;

    } else {
        krberr = ipa_string_to_enctypes(str, &ksdata, &nkeys, err_msg);
        if (krberr) {
            return 0;
        }
    }

    /* Check we don't already have a key with a similar encoding,
     * it would just produce redundant data and this is what the
     * MIT code do anyway */

    for (i = 0, n = 0; i < nkeys; i++ ) {
        krb5_boolean similar = 0;

        for (j = 0; j < i; j++) {
            krberr = krb5_c_enctype_compare(krbctx,
                                            ksdata[j].enctype,
                                            ksdata[i].enctype,
                                            &similar);
            if (krberr) {
                free_keys_contents(krbctx, keys);
                free(ksdata);
                *err_msg = _("Enctype comparison failed!\n");
                return 0;
            }
            if (similar &&
                (ksdata[j].salttype == ksdata[i].salttype)) {
                break;
            }
        }
        if (j < i) {
            /* redundant encoding, remove it, and shift others */
            int x;
            for (x = i; x < nkeys-1; x++) {
                ksdata[x].enctype = ksdata[x+1].enctype;
                ksdata[x].salttype = ksdata[x+1].salttype;
            }
            continue;
        }
        /* count only confirmed enc/salt tuples */
        n++;
    }

    keys->nkeys = n;
    keys->ksdata = ksdata;

    return n;
}

int create_keys(krb5_context krbctx,
                krb5_principal princ,
                char *password,
                const char *enctypes_string,
                struct keys_container *keys,
                char **err_msg)
{
    struct krb_key_salt *ksdata;
    krb5_error_code krberr;
    krb5_data key_password;
    krb5_data *realm = NULL;
    int i, nkeys;
    int ret;

    *err_msg = NULL;

    ret = prep_ksdata(krbctx, enctypes_string, keys, err_msg);
    if (ret == 0) return 0;

    ksdata = keys->ksdata;
    nkeys = keys->nkeys;

    if (password) {
        key_password.data = password;
        key_password.length = strlen(password);
        if (key_password.length > IPAPWD_PASSWORD_MAX_LEN) {
            *err_msg = _("Password is too long!\n");
            return 0;
        }

        realm = krb5_princ_realm(krbctx, princ);
    }

    for (i = 0; i < nkeys; i++) {
        krb5_data *salt;

        if (!password) {
            /* cool, random keys */
            krberr = krb5_c_make_random_key(krbctx,
                                            ksdata[i].enctype,
                                            &ksdata[i].key);
            if (krberr) {
                *err_msg = _("Failed to create random key!\n");
                return 0;
            }
            /* set the salt to NO_SALT as the key was random */
            ksdata[i].salttype = NO_SALT;
            continue;
        }

        /* Make keys using password and required salt */
        switch (ksdata[i].salttype) {
        case KRB5_KDB_SALTTYPE_ONLYREALM:
            krberr = krb5_copy_data(krbctx, realm, &salt);
            if (krberr) {
                *err_msg = _("Failed to create key!\n");
                return 0;
            }

            ksdata[i].salt.length = salt->length;
            ksdata[i].salt.data = malloc(salt->length);
            if (!ksdata[i].salt.data) {
                *err_msg = _("Out of memory!\n");
                return 0;
            }
            memcpy(ksdata[i].salt.data, salt->data, salt->length);
            krb5_free_data(krbctx, salt);
            break;

        case KRB5_KDB_SALTTYPE_NOREALM:
            krberr = ipa_krb5_principal2salt_norealm(krbctx, princ,
                                                     &ksdata[i].salt);
            if (krberr) {
                *err_msg = _("Failed to create key!\n");
                return 0;
            }
            break;

        case KRB5_KDB_SALTTYPE_NORMAL:
            krberr = krb5_principal2salt(krbctx, princ, &ksdata[i].salt);
            if (krberr) {
                *err_msg = _("Failed to create key!\n");
                return 0;
            }
            break;

        /* no KRB5_KDB_SALTTYPE_V4, we do not support krb v4 */

        case KRB5_KDB_SALTTYPE_AFS3:
            /* Comment from MIT sources:
             * * Why do we do this? Well, the afs_mit_string_to_key
             * * needs to use strlen, and the realm is not NULL
             * * terminated....
             */
            ksdata[i].salt.data = (char *)malloc(realm->length + 1);
            if (NULL == ksdata[i].salt.data) {
                *err_msg = _("Out of memory!\n");
                return 0;
            }
            memcpy((char *)ksdata[i].salt.data,
                   (char *)realm->data, realm->length);
            ksdata[i].salt.data[realm->length] = '\0';
            /* AFS uses a special length (UGLY) */
            ksdata[i].salt.length = SALT_TYPE_AFS_LENGTH;
            break;

        default:
            *err_msg = _("Bad or unsupported salt type.\n");
/* FIXME:
            fprintf(stderr, _("Bad or unsupported salt type (%d)!\n"),
                ksdata[i].salttype);
*/
            return 0;
        }

        krberr = krb5_c_string_to_key(krbctx,
                                      ksdata[i].enctype,
                                      &key_password,
                                      &ksdata[i].salt,
                                      &ksdata[i].key);
        if (krberr) {
            *err_msg = _("Failed to create key!\n");
            return 0;
        }

        /* set back salt length to real value if AFS3 */
        if (ksdata[i].salttype == KRB5_KDB_SALTTYPE_AFS3) {
            ksdata[i].salt.length = realm->length;
        }
    }

    return nkeys;
}

/* in older versions of libkrb5 the krb5_salttype_to_string() function is
 * faulty and returns strings that do not match the expected format.
 * Later version of krb5 were fixed to return the proper string.
 * Do lazy detection the first time the function is invoked to determine
 * if we can use the library provided function or if we have to use a
 * fallback map which includes the salt types known up to krb5 1.12 (the
 * fault is fixed upstream in 1.13). */
static int ipa_salttype_to_string(krb5_int32 salttype,
                                  char *buffer, size_t buflen)
{
    static int faulty_function = -1;

    static const struct {
        krb5_int32 salttype;
        const char *name;
    } fallback_map[] = {
        { KRB5_KDB_SALTTYPE_NORMAL, "normal" },
        { KRB5_KDB_SALTTYPE_V4, "v4" },
        { KRB5_KDB_SALTTYPE_NOREALM, "norealm" },
        { KRB5_KDB_SALTTYPE_ONLYREALM, "onlyrealm" },
        { KRB5_KDB_SALTTYPE_SPECIAL, "special" },
        { KRB5_KDB_SALTTYPE_AFS3, "afs3" },
        { -1, NULL }
    };

    if (faulty_function == -1) {
        /* haven't checked yet, let's find out */
        char testbuf[100];
        size_t len = 100;
        int ret;

        ret = krb5_salttype_to_string(KRB5_KDB_SALTTYPE_NORMAL, testbuf, len);
        if (ret) return ret;

        if (strcmp(buffer, "normal") == 0) {
            faulty_function = 0;
        } else {
            faulty_function = 1;
        }
    }

    if (faulty_function == 0) {
        return krb5_salttype_to_string(salttype, buffer, buflen);
    } else {
        size_t len;
        int i;
        for (i = 0; fallback_map[i].name != NULL; i++) {
            if (salttype == fallback_map[i].salttype) break;
        }
        if (fallback_map[i].name == NULL) return EINVAL;

        len = strlen(fallback_map[i].name);
        if (len >= buflen) return ENOMEM;

        memcpy(buffer, fallback_map[i].name, len + 1);
        return 0;
    }
}

int ipa_kstuples_to_string(krb5_key_salt_tuple *kst, int n_kst, char **str)
{
    char *buf = NULL;
    char *tmp;
    int buf_avail;
    int buf_size;
    int buf_cur;
    int len;
    int ret = 0;
    int i;

    buf_size = 512; /* should be enough for the default supported enctypes */
    buf = malloc(buf_size);
    if (!buf) {
        ret = ENOMEM;
        goto done;
    }

    buf_cur = 0;
    for (i = 0; i < n_kst; i++) {
        /* grow if too tight */
        if (ret == ENOMEM) {
            buf_size *= 2;
            /* hard limit at 8k, do not eat all memory by mistake */
            if (buf_size > 8192) goto done;
            tmp = realloc(buf, buf_size);
            if (!tmp) {
                ret = ENOMEM;
                goto done;
            }
            buf = tmp;
        }

        buf_avail = buf_size - buf_cur;
        len = 0;

        /* append separator if necessary */
        if (buf_cur > 0) {
            buf[buf_cur] = ',';
            len++;
        }

        ret = krb5_enctype_to_name(kst[i].ks_enctype, 0,
                                   &buf[buf_cur + len], buf_avail - len);
        if (ret == ENOMEM) {
            i--;
            continue;
        } else if (ret != 0) {
            goto done;
        }

        len += strlen(&buf[buf_cur + len]);
        buf[buf_cur + len] = ':';
        len++;

        ret = ipa_salttype_to_string(kst[i].ks_salttype,
                                     &buf[buf_cur + len], buf_avail - len);
        if (ret == ENOMEM) {
            i--;
            continue;
        } else if (ret != 0) {
            goto done;
        }

        len += strlen(&buf[buf_cur + len]);

        if (buf_avail - len < 2) {
            ret = ENOMEM;
            i--;
            continue;
        }

        buf_cur += len;
    }

    buf[buf_cur] = '\0';
    *str = buf;
    ret = 0;

done:
    if (ret) {
        free(buf);
    }
    return ret;
}
