/*
 * COPYRIGHT (c) International Business Machines Corp. 2012-2017
 *
 * This program is provided under the terms of the Common Public License,
 * version 1.0 (CPL-1.0). Any use, reproduction or distribution for this
 * software constitutes recipient's acceptance of CPL-1.0 terms which can be
 * found in the file LICENSE file or at
 * https://opensource.org/licenses/cpl1.0.php
 */

/*
 * OpenCryptoki ICSF token configuration tool.
 *
 */

#define _GNU_SOURCE
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <getopt.h>
#include <termios.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#include "icsf.h"
#include "slotmgr.h"
#include "pbkdf.h"
#include "defs.h"

#define CFG_ADD         0x0001
#define CFG_LIST        0x0002
#define CFG_BINDDN      0x0004
#define CFG_CERT        0x0008
#define CFG_PRIVKEY     0x0010
#define CFG_CACERT      0x0020
#define CFG_URI         0x0040
#define CFG_MECH        0x0080
#define CFG_MECH_SASL   0x0100
#define CFG_MECH_SIMPLE 0x0200

#define MAX_RECORDS 10
#define SALT_SIZE   16
#define SASL    "sasl"
#define SLOT    "slot"

#define TMPSIZ 64
#define LINESIZ 512
#define TOKBUF  2056
#define STDLL   "libpkcs11_icsf.so"

LDAP *ld;
char *binddn = NULL;
char *uri = NULL;
char *mech = NULL;
char *cert = NULL;
char *cacert = NULL;
char *privkey = NULL;
unsigned long flags = 0;

void usage(char *progname)
{
    printf("usage:\t%s [-h] [ -l | -a token-name] [-b BINDDN]"
           " [-c client-cert-file] [-C CA-cert-file] [-k key] [-u URI]"
           " [-m MECHANISM]\n", progname);
    printf("\t-a add specified token\n");
    printf("\t-b the distinguish name to bind for simple mode\n");
    printf("\t-C the CA certificate file for SASL mode\n");
    printf("\t-c the client certificate file for SASL mode\n");
    printf("\t-h show this help\n");
    printf("\t-k the client private key file for SASL mode\n");
    printf("\t-l list available tokens\n");
    printf("\t-m the authentication mechanism, "
           "it can be 'simple' or 'sasl'\n");
    printf("\t-u the URI to connect to\n");

    exit(-1);
}

int get_pin(char **pin, size_t *pinlen)
{
    struct termios old, new;
    int nread;
    char *buff = NULL;
    size_t buflen;
    int rc = 0;

    /* turn echoing off */
    if (tcgetattr(fileno(stdin), &old) != 0)
        return -1;

    new = old;
    new.c_lflag &= ~ECHO;
    if (tcsetattr(fileno(stdin), TCSAFLUSH, &new) != 0)
        return -1;

    /* read the pin
     * Note: getline will allocate memory for buff. free it when done.
     */
    nread = getline(&buff, &buflen, stdin);
    if (nread == -1) {
        rc = -1;
        goto done;
    }

    /* Restore terminal */
    (void) tcsetattr(fileno(stdin), TCSAFLUSH, &old);

    /* start a newline */
    printf("\n");
    fflush(stdout);

    /* Allocate  PIN.
     * Note: nread includes carriage return.
     * Replace with terminating NULL.
     */
    *pin = (char *) malloc(nread);
    if (*pin == NULL) {
        rc = -ENOMEM;
        goto done;
    }

    /* strip the carriage return since not part of pin. */
    buff[nread - 1] = '\0';
    memcpy(*pin, buff, nread);
    /* don't include the terminating null in the pinlen */
    *pinlen = nread - 1;

done:
    if (buff)
        free(buff);

    return rc;
}

int get_start_slot(void)
{

    FILE *fp;
    char temp[LINESIZ];
    int num;
    int found = -1;
    struct stat statbuf;

    /* if file doesn't exist, then first slot will be 0. */
    if ((stat(OCK_CONFIG, &statbuf) < 0) && (errno == ENOENT))
        return 0;

    fp = fopen(OCK_CONFIG, "r");
    if (fp == NULL) {
        fprintf(stderr, "open failed, line %d: %s\n",
                __LINE__, strerror(errno));
        return -1;
    }

    /* step thru config file and get biggest slot number used */
    while (fgets(temp, LINESIZ, fp) != NULL) {
        if (strstr(temp, SLOT) != NULL) {
            if (sscanf(temp, "%*s %d", &num) == 1)
                if (num > found)
                    found = num;
        }
    }

    /* bump up the slot number, since this will be next new slot entry.
     * if it was an empty file or a file with no slot entries,
     * then next new slot entry will be 0.
     */

    fclose(fp);

    return ++found;
}

int remove_file(char *filename)
{
    struct stat statbuf;

    /* if file exists, then remove it */
    if ((stat(filename, &statbuf) < 0) && (errno == ENOENT)) {
        if (unlink(filename) == -1) {
            fprintf(stderr, "unlink failed for %s, line %d: %s\n",
                    filename, __LINE__, strerror(errno));
            return -1;
        }
    }

    return 0;
}

static void add_token_config_entry(FILE *fp, const char *key, const char *value)
{
    if (!key || !value)
        return;

    fprintf(fp, "%s = \"%s\"\n", key, value);
}

int add_token_config(const char *configname,
                     struct icsf_token_record token, int slot)
{
    FILE *tfp;
    int rc = 0;

    /* create the token config file */
    tfp = fopen(configname, "w");
    if (tfp == NULL) {
        fprintf(stderr, "fopen failed, line %d: %s\n",
                __LINE__, strerror(errno));
        return -1;
    }

    /* add the info */
    fprintf(tfp, "slot %d {\n", slot);
    add_token_config_entry(tfp, "TOKEN_NAME", token.name);
    add_token_config_entry(tfp, "TOKEN_MANUFACTURE", token.manufacturer);
    add_token_config_entry(tfp, "TOKEN_MODEL", token.model);
    add_token_config_entry(tfp, "TOKEN_SERIAL", token.serial);
    add_token_config_entry(tfp, "MECH", (flags & CFG_MECH_SIMPLE)
                           ? "SIMPLE" : "SASL");

    /* add BIND info */
    if (memcmp(mech, "simple", strlen("simple")) == 0) {
        add_token_config_entry(tfp, "BINDDN", binddn);
        add_token_config_entry(tfp, "URI", uri);
    } else {
        add_token_config_entry(tfp, "URI", uri);
        add_token_config_entry(tfp, "CERT", cert);
        add_token_config_entry(tfp, "CACERT", cacert);
        add_token_config_entry(tfp, "KEY", privkey);
    }

    fprintf(tfp, "}\n");

    fflush(tfp);
    if (ferror(tfp) != 0) {
        fprintf(stderr, "failed to add token named, %s\n", token.name);
        rc = -1;
    }

    fclose(tfp);

    return rc;
}

int config_add_slotinfo(int num_of_slots, struct icsf_token_record *tokens)
{
    struct stat statbuf;
    FILE *fp;
    int start_slot = -1;
    char configname[LINESIZ];
    int i, rc;

    /* get the starting slot for next entry */
    start_slot = get_start_slot();
    if (start_slot == -1)
        return -1;

    /* open the config file. if it doesn't exist, create it */
    if ((stat(OCK_CONFIG, &statbuf) == -1) && (errno == ENOENT))
        /* doesn't exist, create it */
        fp = fopen(OCK_CONFIG, "w");
    else
        fp = fopen(OCK_CONFIG, "a");

    if (fp == NULL) {
        fprintf(stderr, "open failed, line %d: %s\n",
                __LINE__, strerror(errno));
        return -1;
    }

    /* For each token in the list do,
     *      - Create a slot entry in ock config file that contains
     *        the stdll and token config name.
     *      - Create a token config file that contains the token info
     *        from the ICSF and the BIND authentication info.
     */
    for (i = 0; i < num_of_slots; i++) {

        /* create the config name using the token's name */
        memset(configname, 0, sizeof(configname));
        snprintf(configname, sizeof(configname), "%s/%s.conf",
                 OCK_CONFDIR, tokens[i].name);

        /* write the token info to the token's config file */
        rc = add_token_config(configname, tokens[i], start_slot);
        if (rc == -1) {
            fprintf(stderr, "failed to add %s token.\n", tokens[i].name);
            /* skip adding this entry */
            continue;
        }

        /* add the slot entry to the ock config file */
        fprintf(fp, "\nslot %d {\n", start_slot);
        fprintf(fp, "stdll = %s\n", STDLL);
        fprintf(fp, "confname = %s\n", configname);
        fprintf(fp, "}\n");
        fflush(fp);
        if (ferror(fp) != 0) {
            fprintf(stderr, "Failed to add an entry for %s token: "
                    "%s\n", tokens[i].name, strerror(errno));
            remove_file(configname);
            continue;
        }

        /* bump the slot number */
        start_slot++;
    }

    fclose(fp);

    return 0;
}

int list_tokens(void)
{
    size_t i, tokenCount = MAX_RECORDS;
    struct icsf_token_record *previous = NULL;
    struct icsf_token_record tokens[MAX_RECORDS];
    int rc, num_seen = 0;

    do {
        /* get the token list from remote z/OS host */
        rc = icsf_list_tokens(ld, NULL, previous, tokens, &tokenCount);
        if (ICSF_RC_IS_ERROR(rc))
            return -1;

        for (i = 0; i < tokenCount; i++) {
            printf("Token #:      %d\n"
                   "Token name:   %s\n"
                   "Manufacturer: %s\n"
                   "Model:        %s\n"
                   "Serial:       %s\n"
                   "Read-only:    %s\n\n",
                   num_seen, tokens[i].name,
                   tokens[i].manufacturer,
                   tokens[i].model, tokens[i].serial,
                   ICSF_IS_TOKEN_READ_ONLY(tokens[i].flags) ? "yes" : "no");
            num_seen++;
        }

        if (tokenCount)
            previous = &tokens[tokenCount - 1];

    } while (tokenCount);

    return 0;
}

int lookup_name(char *name, struct icsf_token_record *found)
{
    size_t i, tokenCount = MAX_RECORDS;
    struct icsf_token_record *previous = NULL;
    struct icsf_token_record tokens[MAX_RECORDS];
    int rc;

    do {
        /* get the token list from remote z/OS host */
        rc = icsf_list_tokens(ld, NULL, previous, tokens, &tokenCount);
        if (ICSF_RC_IS_ERROR(rc)) {
            fprintf(stderr, "Could not get list of tokens.\n");
            found = NULL;
            return -1;
        }

        for (i = 0; i < tokenCount; i++) {
            if (strncasecmp(name, tokens[i].name,
                            sizeof(tokens[i].name)) == 0) {
                memcpy(found, &tokens[i], sizeof(struct icsf_token_record));
                return 0;
            }
        }
        if (tokenCount)
            previous = &tokens[tokenCount - 1];

    } while (tokenCount);

    /* if we get here, we could not find the token in the list. */
    found = NULL;

    return -1;
}

void remove_racf_file(void)
{
    char fname[PATH_MAX];

    /* remove the so and user files */
    snprintf(fname, sizeof(fname), "%s/RACF", ICSF_CONFIG_PATH);
    remove_file(fname);
}

int retrieve_all(void)
{
    size_t tokenCount;
    struct icsf_token_record *previous = NULL;
    struct icsf_token_record tokens[MAX_RECORDS];
    int rc;


    /* since pkcsslotd can only manage
     * NUMBER_SLOTS_MANAGED at a time, use this as
     * the maxiumum amount of tokens to retrieve...
     */
    tokenCount = NUMBER_SLOTS_MANAGED;
    rc = icsf_list_tokens(ld, NULL, previous, tokens, &tokenCount);
    if (ICSF_RC_IS_ERROR(rc)) {
        fprintf(stderr, "Could not get list of tokens.\n");
        return -1;
    }

    /* add slot and token entry(ies) */
    rc = config_add_slotinfo(tokenCount, tokens);
    if (rc) {
        fprintf(stderr, "Could not add list of tokens.\n");
        return -1;
    }

    return 0;
}

int secure_racf_passwd(char *racfpwd, unsigned int len)
{
    char *sopin = NULL;
    unsigned char masterkey[AES_KEY_SIZE_256];
    char fname[PATH_MAX];
    int rc;
    size_t sopinlen;


    /* get the SO PIN */
    printf("Enter the SO PIN: ");
    fflush(stdout);
    rc = get_pin(&sopin, &sopinlen);
    if (rc != 0) {
        fprintf(stderr, "Could not get SO PIN.\n");
        rc = -1;
        goto cleanup;
    }

    /* generate a masterkey */
    if ((get_randombytes(masterkey, AES_KEY_SIZE_256)) != CKR_OK) {
        fprintf(stderr, "Could not generate masterkey.\n");
        rc = -1;
        goto cleanup;
    }

    /* use the master key to secure the racf passwd */
    rc = secure_racf((CK_BYTE *)racfpwd, len, masterkey, AES_KEY_SIZE_256);
    if (rc != 0) {
        fprintf(stderr, "Failed to secure racf passwd.\n");
        rc = -1;
        goto cleanup;
    }

    /* now secure the master key with a derived key */
    /* first get the filename to put the  encrypted masterkey */
    snprintf(fname, sizeof(fname), "%s/MK_SO", ICSF_CONFIG_PATH);
    rc = secure_masterkey(masterkey, AES_KEY_SIZE_256, (CK_BYTE *)sopin,
                          strlen(sopin), fname);

    if (rc != 0) {
        fprintf(stderr, "Failed to secure masterkey.\n");
        /* remove the racf file */
        remove_racf_file();
        rc = -1;
        goto cleanup;
    }

cleanup:
    if (sopin)
        free(sopin);

    return rc;
}

int main(int argc, char **argv)
{
    char *racfpwd = NULL;
    size_t racflen;
    char *tokenname = NULL;
    int c;
    int rc = 0;
    struct icsf_token_record found_token;

    while ((c = getopt(argc, argv, "hla:b:u:m:k:c:C:")) != (-1)) {
        switch (c) {
        case 'a':
            flags |= CFG_ADD;
            if ((tokenname = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'l':
            flags |= CFG_LIST;
            break;
        case 'b':
            flags |= CFG_BINDDN;
            if ((binddn = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'c':
            flags |= CFG_CERT;
            if ((cert = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'k':
            flags |= CFG_PRIVKEY;
            if ((privkey = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'C':
            flags |= CFG_CACERT;
            if ((cacert = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'u':
            flags |= CFG_URI;
            if ((uri = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            break;
        case 'm':
            flags |= CFG_MECH;
            if ((mech = strdup(optarg)) == NULL) {
                rc = -1;
                fprintf(stderr, "strdup failed: line %d\n", __LINE__);
                goto cleanup;
            }
            if (memcmp(mech, SASL, sizeof(SASL)) == 0)
                flags |= CFG_MECH_SASL;
            else
                flags |= CFG_MECH_SIMPLE;
            break;
        case 'h':
        default:
            usage(argv[0]);
            break;
        }
    }

    /* Noticed that if a user misses an argument after an option,
     * sometimes getopt misses it.
     * For example, pkcsiscf -a -m -b xxxx -u xxxx"
     * To catch these anomalies, check that optind == argc.
     */
    if (optind != argc)
        usage(argv[0]);

    /* If there were no options, print usage. */
    if ((!flags) || (!(flags & CFG_ADD) && !(flags & CFG_LIST)))
        usage(argv[0]);

    /* Currently, do not allow user to add a list of tokens.
     * When ready to support multiple icsf tokens, this
     * check can be removed.
     */
    if ((flags & CFG_ADD) && !(memcmp(tokenname, "all", strlen(tokenname))))
        usage(argv[0]);

    /* If add, then must specify a mechanism and a name */
    if ((flags & CFG_ADD) && (!(flags & CFG_MECH) || tokenname == NULL))
        usage(argv[0]);

    /* If list, then must specify a mechanism */
    if ((flags & CFG_LIST) && !(flags & CFG_MECH))
        usage(argv[0]);

    /* Cannot add and list at the same time */
    if ((flags & CFG_LIST) && (flags & CFG_ADD))
        usage(argv[0]);

    /* May only specify one mechanism */
    if ((flags & CFG_MECH_SASL) && (flags & CFG_MECH_SIMPLE))
        usage(argv[0]);

    /* Cannot specify bind DN with SASL */
    if ((flags & CFG_MECH_SASL) && (flags & CFG_BINDDN))
        usage(argv[0]);

    /* Cannot specify certs or key with SIMPLE */
    if ((flags & CFG_MECH_SIMPLE)
        && (flags & (CFG_CERT | CFG_PRIVKEY | CFG_CACERT)))
        usage(argv[0]);

    /* get racf password if needed */
    if ((flags & CFG_ADD) || (flags & CFG_LIST)) {
        if (flags & CFG_MECH_SIMPLE) {
            printf("Enter the RACF passwd: ");
            fflush(stdout);
            rc = get_pin(&racfpwd, &racflen);
            if (rc != 0) {
                fprintf(stderr, "Could not get RACF passwd.\n");
                return -1;
            }

            /* bind to ldap server */
            rc = icsf_login(&ld, uri, binddn, racfpwd);
        } else {
            rc = icsf_sasl_login(&ld, uri, NULL, NULL, NULL, NULL);
        }
        if (rc) {
            fprintf(stderr, "Failed to bind to the ldap server.\n");
            goto cleanup;
        }
    }


    /* Add token(s) */
    if (flags & CFG_ADD) {
        if (memcmp(tokenname, "all", strlen(tokenname)) == 0) {
            rc = retrieve_all();
            if (rc) {
                fprintf(stderr, "Could not add the list of " "tokens.\n");
                goto cleanup;
            }
        } else {
            /* add only the specified tokenname.
             * first, find it in the list.
             */
            rc = lookup_name(tokenname, &found_token);
            if (rc != 0) {
                fprintf(stderr,
                        "Could not find %s in token list.\n", tokenname);
                rc = -1;
                goto cleanup;
            }

            /* add the entry */
            rc = config_add_slotinfo(1, &found_token);
            if (rc != 0)
                goto cleanup;
        }
        if (flags & CFG_MECH_SIMPLE) {
            /* when using simple auth, secure racf passwd. */
            rc = secure_racf_passwd(racfpwd, strlen(racfpwd));
            if (rc != 0)
                goto cleanup;
        }
    }

    if (flags & CFG_LIST) {
        /* print the list of available tokens */
        rc = list_tokens();
        if (rc != 0)
            fprintf(stderr, "Could not get full list of tokens.\n");
    }

cleanup:
    if (ld)
        icsf_logout(ld);
    if (tokenname)
        free(tokenname);
    if (binddn)
        free(binddn);
    if (cert)
        free(cert);
    if (privkey)
        free(privkey);
    if (cacert)
        free(cacert);
    if (uri)
        free(uri);
    if (mech)
        free(mech);
    if (racfpwd)
        free(racfpwd);

    return rc;
}
