/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* plugins/preauth/otp/otp_state.c - Verify OTP token values using RADIUS */
/*
 * Copyright 2013 Red Hat, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "otp_state.h"

#include <krad.h>
#include <k5-json.h>

#include <ctype.h>

#ifndef HOST_NAME_MAX
/* SUSv2 */
#define HOST_NAME_MAX 255
#endif

#define DEFAULT_TYPE_NAME "DEFAULT"
#define DEFAULT_SOCKET_FMT KDC_RUN_DIR "/%s.socket"
#define DEFAULT_TIMEOUT 5
#define DEFAULT_RETRIES 3
#define MAX_SECRET_LEN 1024

typedef struct token_type_st {
    char *name;
    char *server;
    char *secret;
    int timeout;
    size_t retries;
    krb5_boolean strip_realm;
} token_type;

typedef struct token_st {
    const token_type *type;
    krb5_data username;
} token;

typedef struct request_st {
    otp_state *state;
    token *tokens;
    ssize_t index;
    otp_cb cb;
    void *data;
    krad_attrset *attrs;
} request;

struct otp_state_st {
    krb5_context ctx;
    token_type *types;
    krad_client *radius;
    krad_attrset *attrs;
};

static void request_send(request *req);

static krb5_error_code
read_secret_file(const char *secret_file, char **secret)
{
    char buf[MAX_SECRET_LEN];
    krb5_error_code retval;
    char *filename;
    FILE *file;
    int i, j;

    *secret = NULL;

    retval = k5_path_join(KDC_DIR, secret_file, &filename);
    if (retval != 0) {
        com_err("otp", retval, "Unable to resolve secret file '%s'", filename);
        return retval;
    }

    file = fopen(filename, "r");
    if (file == NULL) {
        retval = errno;
        com_err("otp", retval, "Unable to open secret file '%s'", filename);
        return retval;
    }

    if (fgets(buf, sizeof(buf), file) == NULL)
        retval = EIO;
    fclose(file);
    if (retval != 0) {
        com_err("otp", retval, "Unable to read secret file '%s'", filename);
        return retval;
    }

    /* Strip whitespace. */
    for (i = 0; buf[i] != '\0'; i++) {
        if (!isspace(buf[i]))
            break;
    }
    for (j = strlen(buf) - i; j > 0; j--) {
        if (!isspace(buf[j - 1]))
            break;
    }

    *secret = k5memdup0(&buf[i], j - i, &retval);
    return retval;
}

/* Free the contents of a single token type. */
static void
token_type_free(token_type *type)
{
    if (type == NULL)
        return;

    free(type->name);
    free(type->server);
    free(type->secret);
}

/* Construct the internal default token type. */
static krb5_error_code
token_type_default(token_type *out)
{
    char *name = NULL, *server = NULL, *secret = NULL;

    memset(out, 0, sizeof(*out));

    name = strdup(DEFAULT_TYPE_NAME);
    if (name == NULL)
        goto oom;
    if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0)
        goto oom;
    secret = strdup("");
    if (secret == NULL)
        goto oom;

    out->name = name;
    out->server = server;
    out->secret = secret;
    out->timeout = DEFAULT_TIMEOUT * 1000;
    out->retries = DEFAULT_RETRIES;
    out->strip_realm = FALSE;
    return 0;

oom:
    free(name);
    free(server);
    free(secret);
    return ENOMEM;
}

/* Decode a single token type from the profile. */
static krb5_error_code
token_type_decode(profile_t profile, const char *name, token_type *out)
{
    char *server = NULL, *name_copy = NULL, *secret = NULL, *pstr = NULL;
    int strip_realm, timeout, retries;
    krb5_error_code retval;

    memset(out, 0, sizeof(*out));

    /* Set the name. */
    name_copy = strdup(name);
    if (name_copy == NULL)
        return ENOMEM;

    /* Set strip_realm. */
    retval = profile_get_boolean(profile, "otp", name, "strip_realm", TRUE,
                                 &strip_realm);
    if (retval != 0)
        goto cleanup;

    /* Set the server. */
    retval = profile_get_string(profile, "otp", name, "server", NULL, &pstr);
    if (retval != 0)
        goto cleanup;
    if (pstr != NULL) {
        server = strdup(pstr);
        profile_release_string(pstr);
        if (server == NULL) {
            retval = ENOMEM;
            goto cleanup;
        }
    } else if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0) {
        retval = ENOMEM;
        goto cleanup;
    }

    /* Get the secret (optional for Unix-domain sockets). */
    retval = profile_get_string(profile, "otp", name, "secret", NULL, &pstr);
    if (retval != 0)
        goto cleanup;
    if (pstr != NULL) {
        retval = read_secret_file(pstr, &secret);
        profile_release_string(pstr);
        if (retval != 0)
            goto cleanup;
    } else {
        if (server[0] != '/') {
            com_err("otp", EINVAL, "Secret missing (token type '%s')", name);
            retval = EINVAL;
            goto cleanup;
        }

        /* Use the default empty secret for UNIX domain stream sockets. */
        secret = strdup("");
        if (secret == NULL) {
            retval = ENOMEM;
            goto cleanup;
        }
    }

    /* Get the timeout (profile value in seconds, result in milliseconds). */
    retval = profile_get_integer(profile, "otp", name, "timeout",
                                 DEFAULT_TIMEOUT, &timeout);
    if (retval != 0)
        goto cleanup;
    timeout *= 1000;

    /* Get the number of retries. */
    retval = profile_get_integer(profile, "otp", name, "retries",
                                 DEFAULT_RETRIES, &retries);
    if (retval != 0)
        goto cleanup;

    out->name = name_copy;
    out->server = server;
    out->secret = secret;
    out->timeout = timeout;
    out->retries = retries;
    out->strip_realm = strip_realm;
    name_copy = server = secret = NULL;

cleanup:
    free(name_copy);
    free(server);
    free(secret);
    return retval;
}

/* Free an array of token types. */
static void
token_types_free(token_type *types)
{
    size_t i;

    if (types == NULL)
        return;

    for (i = 0; types[i].server != NULL; i++)
        token_type_free(&types[i]);

    free(types);
}

/* Decode an array of token types from the profile. */
static krb5_error_code
token_types_decode(profile_t profile, token_type **out)
{
    const char *hier[2] = { "otp", NULL };
    token_type *types = NULL;
    char **names = NULL;
    krb5_error_code retval;
    size_t i, pos;
    krb5_boolean have_default = FALSE;

    retval = profile_get_subsection_names(profile, hier, &names);
    if (retval != 0)
        return retval;

    /* Check if any of the profile subsections overrides the default. */
    for (i = 0; names[i] != NULL; i++) {
        if (strcmp(names[i], DEFAULT_TYPE_NAME) == 0)
            have_default = TRUE;
    }

    /* Leave space for the default (possibly) and the terminator. */
    types = k5calloc(i + 2, sizeof(token_type), &retval);
    if (types == NULL)
        goto cleanup;

    /* If no default has been specified, use our internal default. */
    pos = 0;
    if (!have_default) {
        retval = token_type_default(&types[pos++]);
        if (retval != 0)
            goto cleanup;
    }

    /* Decode each profile section into a token type element. */
    for (i = 0; names[i] != NULL; i++) {
        retval = token_type_decode(profile, names[i], &types[pos++]);
        if (retval != 0)
            goto cleanup;
    }

    *out = types;
    types = NULL;

cleanup:
    profile_free_list(names);
    token_types_free(types);
    return retval;
}

/* Free the contents of a single token. */
static void
token_free_contents(token *t)
{
    if (t != NULL)
        free(t->username.data);
}

/* Decode a single token from a JSON token object. */
static krb5_error_code
token_decode(krb5_context ctx, krb5_const_principal princ,
             const token_type *types, k5_json_object obj, token *out)
{
    const char *typename = DEFAULT_TYPE_NAME;
    const token_type *type = NULL;
    char *username = NULL;
    krb5_error_code retval;
    k5_json_value val;
    size_t i;
    int flags;

    memset(out, 0, sizeof(*out));

    /* Find the token type. */
    val = k5_json_object_get(obj, "type");
    if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING)
        typename = k5_json_string_utf8(val);
    for (i = 0; types[i].server != NULL; i++) {
        if (strcmp(typename, types[i].name) == 0)
            type = &types[i];
    }
    if (type == NULL)
        return EINVAL;

    /* Get the username, either from obj or from unparsing the principal. */
    val = k5_json_object_get(obj, "username");
    if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING) {
        username = strdup(k5_json_string_utf8(val));
        if (username == NULL)
            return ENOMEM;
    } else {
        flags = type->strip_realm ? KRB5_PRINCIPAL_UNPARSE_NO_REALM : 0;
        retval = krb5_unparse_name_flags(ctx, princ, flags, &username);
        if (retval != 0)
            return retval;
    }

    out->type = type;
    out->username = string2data(username);
    return 0;
}

/* Free an array of tokens. */
static void
tokens_free(token *tokens)
{
    size_t i;

    if (tokens == NULL)
        return;

    for (i = 0; tokens[i].type != NULL; i++)
        token_free_contents(&tokens[i]);

    free(tokens);
}

/* Decode a principal config string into a JSON array.  Treat an empty string
 * or array as if it were "[{}]" which uses the default token type. */
static krb5_error_code
decode_config_json(const char *config, k5_json_array *out)
{
    krb5_error_code retval;
    k5_json_value val;
    k5_json_object obj;

    *out = NULL;

    /* Decode the config string and make sure it's an array. */
    retval = k5_json_decode((config != NULL) ? config : "[{}]", &val);
    if (retval != 0)
        goto error;
    if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) {
        retval = EINVAL;
        goto error;
    }

    /* If the array is empty, add in an empty object. */
    if (k5_json_array_length(val) == 0) {
        retval = k5_json_object_create(&obj);
        if (retval != 0)
            goto error;
        retval = k5_json_array_add(val, obj);
        k5_json_release(obj);
        if (retval != 0)
            goto error;
    }

    *out = val;
    return 0;

error:
    k5_json_release(val);
    return retval;
}

/* Decode an array of tokens from the configuration string. */
static krb5_error_code
tokens_decode(krb5_context ctx, krb5_const_principal princ,
              const token_type *types, const char *config, token **out)
{
    krb5_error_code retval;
    k5_json_array arr = NULL;
    k5_json_value obj;
    token *tokens = NULL;
    size_t len, i;

    retval = decode_config_json(config, &arr);
    if (retval != 0)
        return retval;
    len = k5_json_array_length(arr);

    tokens = k5calloc(len + 1, sizeof(token), &retval);
    if (tokens == NULL)
        goto cleanup;

    for (i = 0; i < len; i++) {
        obj = k5_json_array_get(arr, i);
        if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT) {
            retval = EINVAL;
            goto cleanup;
        }
        retval = token_decode(ctx, princ, types, obj, &tokens[i]);
        if (retval != 0)
            goto cleanup;
    }

    *out = tokens;
    tokens = NULL;

cleanup:
    k5_json_release(arr);
    tokens_free(tokens);
    return retval;
}

static void
request_free(request *req)
{
    if (req == NULL)
        return;

    krad_attrset_free(req->attrs);
    tokens_free(req->tokens);
    free(req);
}

krb5_error_code
otp_state_new(krb5_context ctx, otp_state **out)
{
    char hostname[HOST_NAME_MAX + 1];
    krb5_error_code retval;
    profile_t profile;
    krb5_data hndata;
    otp_state *self;

    retval = gethostname(hostname, sizeof(hostname));
    if (retval != 0)
        return retval;

    self = calloc(1, sizeof(otp_state));
    if (self == NULL)
        return ENOMEM;

    retval = krb5_get_profile(ctx, &profile);
    if (retval != 0)
        goto error;

    retval = token_types_decode(profile, &self->types);
    profile_abandon(profile);
    if (retval != 0)
        goto error;

    retval = krad_attrset_new(ctx, &self->attrs);
    if (retval != 0)
        goto error;

    hndata = make_data(hostname, strlen(hostname));
    retval = krad_attrset_add(self->attrs,
                              krad_attr_name2num("NAS-Identifier"), &hndata);
    if (retval != 0)
        goto error;

    retval = krad_attrset_add_number(self->attrs,
                                     krad_attr_name2num("Service-Type"),
                                     KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY);
    if (retval != 0)
        goto error;

    self->ctx = ctx;
    *out = self;
    return 0;

error:
    otp_state_free(self);
    return retval;
}

void
otp_state_free(otp_state *self)
{
    if (self == NULL)
        return;

    krad_attrset_free(self->attrs);
    token_types_free(self->types);
    free(self);
}

static void
callback(krb5_error_code retval, const krad_packet *rqst,
         const krad_packet *resp, void *data)
{
    request *req = data;

    req->index++;

    if (retval != 0)
        goto error;

    /* If we received an accept packet, success! */
    if (krad_packet_get_code(resp) ==
        krad_code_name2num("Access-Accept")) {
        req->cb(req->data, retval, otp_response_success);
        request_free(req);
        return;
    }

    /* If we have no more tokens to try, failure! */
    if (req->tokens[req->index].type == NULL)
        goto error;

    /* Try the next token. */
    request_send(req);

error:
    req->cb(req->data, retval, otp_response_fail);
    request_free(req);
}

static void
request_send(request *req)
{
    krb5_error_code retval;
    token *tok = &req->tokens[req->index];
    const token_type *t = tok->type;

    retval = krad_attrset_add(req->attrs, krad_attr_name2num("User-Name"),
                              &tok->username);
    if (retval != 0)
        goto error;

    retval = krad_client_send(req->state->radius,
                              krad_code_name2num("Access-Request"), req->attrs,
                              t->server, t->secret, t->timeout, t->retries,
                              callback, req);
    krad_attrset_del(req->attrs, krad_attr_name2num("User-Name"), 0);
    if (retval != 0)
        goto error;

    return;

error:
    req->cb(req->data, retval, otp_response_fail);
    request_free(req);
}

void
otp_state_verify(otp_state *state, verto_ctx *ctx, krb5_const_principal princ,
                 const char *config, const krb5_pa_otp_req *req,
                 otp_cb cb, void *data)
{
    krb5_error_code retval;
    request *rqst = NULL;
    char *name;

    if (state->radius == NULL) {
        retval = krad_client_new(state->ctx, ctx, &state->radius);
        if (retval != 0)
            goto error;
    }

    rqst = calloc(1, sizeof(request));
    if (rqst == NULL) {
        (*cb)(data, ENOMEM, otp_response_fail);
        return;
    }
    rqst->state = state;
    rqst->data = data;
    rqst->cb = cb;

    retval = krad_attrset_copy(state->attrs, &rqst->attrs);
    if (retval != 0)
        goto error;

    retval = krad_attrset_add(rqst->attrs, krad_attr_name2num("User-Password"),
                              &req->otp_value);
    if (retval != 0)
        goto error;

    retval = tokens_decode(state->ctx, princ, state->types, config,
                           &rqst->tokens);
    if (retval != 0) {
        if (krb5_unparse_name(state->ctx, princ, &name) == 0) {
            com_err("otp", retval,
                    "Can't decode otp config string for principal '%s'", name);
            krb5_free_unparsed_name(state->ctx, name);
        }
        goto error;
    }

    request_send(rqst);
    return;

error:
    (*cb)(data, retval, otp_response_fail);
    request_free(rqst);
}
