/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "mod_session.h"
#include "apu_version.h"
#include "apr_base64.h"                /* for apr_base64_decode et al */
#include "apr_lib.h"
#include "apr_strings.h"
#include "http_log.h"
#include "http_core.h"

#if APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION < 4

#error session_crypto_module requires APU v1.4.0 or later

#elif APU_HAVE_CRYPTO == 0

#error Crypto support must be enabled in APR

#else

#if APR_MAJOR_VERSION < 2
#define CRYPTO_VERSION 104
#else
#define CRYPTO_VERSION 200
#endif

#include "apr_crypto.h"                /* for apr_*_crypt et al */

#define LOG_PREFIX "mod_session_crypto: "
#define DRIVER_KEY "session_crypto_driver"

module AP_MODULE_DECLARE_DATA session_crypto_module;

/**
 * Structure to carry the per-dir session config.
 */
typedef struct {
    const char *passphrase;
    apr_array_header_t *params;
    int passphrase_set;
    apr_crypto_block_key_type_e cipher;
    int cipher_set;
}session_crypto_dir_conf;

/**
 * Structure to carry the server wide session config.
 */
typedef struct {
    const char *library;
    apr_array_header_t *params;
    int library_set;
    int noinit;
    int noinit_set;
}session_crypto_conf;

AP_DECLARE(int) ap_session_crypto_encode(request_rec * r, session_rec * z);
AP_DECLARE(int) ap_session_crypto_decode(request_rec * r, session_rec * z);
AP_DECLARE(int) ap_session_crypto_init(apr_pool_t *p, apr_pool_t *plog,
        apr_pool_t *ptemp, server_rec *s);

/**
 * Initialise the encryption as per the current config.
 *
 * Returns APR_SUCCESS if successful.
 */
static apr_status_t crypt_init(request_rec * r,
                               const apr_crypto_driver_t *driver,
                               apr_crypto_t **f, apr_crypto_key_t **key,
                               apr_uuid_t *salt, apr_size_t *ivSize,
                               session_crypto_dir_conf * dconf)
{
    apr_status_t res;

    if (!driver) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, LOG_PREFIX
                "encryption driver not configured, "
                "no SessionCryptoDriver set");
        return APR_EGENERAL;
    }

    if (!dconf->passphrase_set) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, LOG_PREFIX
                "encryption not configured, "
                "no passphrase set");
        return APR_EGENERAL;
    }

    /* set up */
    res = apr_crypto_make(driver, r->pool, dconf->params, f);
    if (APR_ENOTIMPL == res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "generic symmetrical encryption is not supported by this "
                "version of APR. session encryption not possible");
    }

    if (APR_SUCCESS == res) {
#if CRYPTO_VERSION < 200
        res = apr_crypto_passphrase(driver, r->pool, *f, dconf->passphrase,
#else
        res = apr_crypto_passphrase(r->pool, *f, dconf->passphrase,
#endif
                strlen(dconf->passphrase),
                (unsigned char *) salt, salt ? sizeof(apr_uuid_t) : 0,
                dconf->cipher, MODE_CBC, 1, 4096, key, ivSize);
    }

    if (APR_STATUS_IS_ENOKEY(res)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "the passphrase '%s' was empty", dconf->passphrase);
    }
    if (APR_STATUS_IS_EPADDING(res)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "padding is not supported for cipher");
    }
    if (APR_STATUS_IS_EKEYTYPE(res)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "the key type is not known");
    }
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "encryption could not be configured. Please check the "
                "certificates and/or passphrase as appropriate");
        return APR_EGENERAL;
    }

    return APR_SUCCESS;
}

/**
 * Encrypt the string given as per the current config.
 *
 * Returns APR_SUCCESS if successful.
 */
static apr_status_t encrypt_string(request_rec * r,
                                   const apr_crypto_driver_t *driver,
                                   session_crypto_dir_conf *dconf,
                                   const char *in, char **out)
{
    apr_status_t res;
    apr_crypto_t *f = NULL;
    apr_crypto_key_t *key = NULL;
    apr_size_t ivSize = 0;
    apr_crypto_block_t *block = NULL;
    unsigned char *encrypt = NULL;
    unsigned char *combined = NULL;
    apr_size_t encryptlen, tlen;
    char *base64;
    apr_size_t blockSize = 0;
    const unsigned char *iv = NULL;
    apr_uuid_t salt;

    /* by default, return an empty string */
    *out = "";

    /* don't attempt to encrypt an empty string, trying to do so causes a segfault */
    if (!in || !*in) {
        return APR_SUCCESS;
    }

    /* use a uuid as a salt value, and prepend it to our result */
    apr_uuid_get(&salt);
    res = crypt_init(r, driver, &f, &key, &salt, &ivSize, dconf);
    if (res != APR_SUCCESS) {
        return res;
    }

#if CRYPTO_VERSION < 200
    res = apr_crypto_block_encrypt_init(driver, r->pool, f, key, &iv, &block,
#else
    res = apr_crypto_block_encrypt_init(r->pool, f, key, &iv, &block,
#endif
            &blockSize);
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_encrypt_init failed");
        return res;
    }

    /* encrypt the given string */
#if CRYPTO_VERSION < 200
    res = apr_crypto_block_encrypt(driver, block, &encrypt,
#else
    res = apr_crypto_block_encrypt(f, block, &encrypt,
#endif
            &encryptlen, (unsigned char *)in, strlen(in));
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_encrypt failed");
        return res;
    }
#if CRYPTO_VERSION < 200
    res = apr_crypto_block_encrypt_finish(driver, block, encrypt + encryptlen,
#else
    res = apr_crypto_block_encrypt_finish(f, block, encrypt + encryptlen,
#endif
            &tlen);
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_encrypt_finish failed");
        return res;
    }
    encryptlen += tlen;

    /* prepend the salt and the iv to the result */
    combined = apr_palloc(r->pool, ivSize + encryptlen + sizeof(apr_uuid_t));
    memcpy(combined, &salt, sizeof(apr_uuid_t));
    memcpy(combined + sizeof(apr_uuid_t), iv, ivSize);
    memcpy(combined + sizeof(apr_uuid_t) + ivSize, encrypt, encryptlen);

    /* base64 encode the result */
    base64 = apr_palloc(r->pool, apr_base64_encode_len(ivSize + encryptlen +
                                                       sizeof(apr_uuid_t) + 1)
                                 * sizeof(char));
    apr_base64_encode(base64, (const char *) combined,
                      ivSize + encryptlen + sizeof(apr_uuid_t));
    *out = base64;

    return res;

}

/**
 * Decrypt the string given as per the current config.
 *
 * Returns APR_SUCCESS if successful.
 */
static apr_status_t decrypt_string(request_rec * r,
                                   const apr_crypto_driver_t *driver,
                                   session_crypto_dir_conf *dconf,
                                   const char *in, char **out)
{
    apr_status_t res;
    apr_crypto_t *f = NULL;
    apr_crypto_key_t *key = NULL;
    apr_size_t ivSize = 0;
    apr_crypto_block_t *block = NULL;
    unsigned char *decrypted = NULL;
    apr_size_t decryptedlen, tlen;
    apr_size_t decodedlen;
    char *decoded;
    apr_size_t blockSize = 0;

    /* strip base64 from the string */
    decoded = apr_palloc(r->pool, apr_base64_decode_len(in));
    decodedlen = apr_base64_decode(decoded, in);
    decoded[decodedlen] = '\0';

    res = crypt_init(r, driver, &f, &key, (apr_uuid_t *)decoded, &ivSize, dconf);
    if (res != APR_SUCCESS) {
        return res;
    }

    /* sanity check - decoded too short? */
    if (decodedlen < (sizeof(apr_uuid_t) + ivSize)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_SUCCESS, r, LOG_PREFIX
                "too short to decrypt, skipping");
        return APR_ECRYPT;
    }

    /* bypass the salt at the start of the decoded block */
    decoded += sizeof(apr_uuid_t);
    decodedlen -= sizeof(apr_uuid_t);

#if CRYPTO_VERSION < 200
    res = apr_crypto_block_decrypt_init(driver, r->pool, f, key, (unsigned char *)decoded, &block,
#else
    res = apr_crypto_block_decrypt_init(r->pool, f, key, (unsigned char *)decoded, &block,
#endif
            &blockSize);
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_decrypt_init failed");
        return res;
    }

    /* bypass the iv at the start of the decoded block */
    decoded += ivSize;
    decodedlen -= ivSize;

    /* decrypt the given string */
#if CRYPTO_VERSION < 200
    res = apr_crypto_block_decrypt(driver, block, &decrypted,
#else
    res = apr_crypto_block_decrypt(f, block, &decrypted,
#endif
            &decryptedlen, (unsigned char *)decoded, decodedlen);
    if (res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_decrypt failed");
        return res;
    }
    *out = (char *) decrypted;

#if CRYPTO_VERSION < 200
    res = apr_crypto_block_decrypt_finish(driver, block, decrypted + decryptedlen,
#else
    res = apr_crypto_block_decrypt_finish(f, block, decrypted + decryptedlen,
#endif
            &tlen);
    if (APR_SUCCESS != res) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                "apr_crypto_block_decrypt_finish failed");
        return res;
    }
    decryptedlen += tlen;
    decrypted[decryptedlen] = 0;

    return APR_SUCCESS;

}

/**
 * Crypto encoding for the session.
 *
 * @param r The request pointer.
 * @param z A pointer to where the session will be written.
 */
AP_DECLARE(int) ap_session_crypto_encode(request_rec * r, session_rec * z)
{

    char *encoded = NULL;
    apr_status_t res;
    const apr_crypto_driver_t *driver = NULL;
    session_crypto_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
            &session_crypto_module);

    if (dconf->passphrase_set && z->encoded && *z->encoded) {
        apr_pool_userdata_get((void **)&driver, DRIVER_KEY, r->server->process->pconf);
        res = encrypt_string(r, driver, dconf, z->encoded, &encoded);
        if (res != OK) {
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, LOG_PREFIX
                    "encrypt session failed");
            return res;
        }
        z->encoded = encoded;
    }

    return OK;

}

/**
 * Crypto decoding for the session.
 *
 * @param r The request pointer.
 * @param z A pointer to where the session will be written.
 */
AP_DECLARE(int) ap_session_crypto_decode(request_rec * r, session_rec * z)
{

    char *encoded = NULL;
    apr_status_t res;
    const apr_crypto_driver_t *driver = NULL;
    session_crypto_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
            &session_crypto_module);

    if ((dconf->passphrase_set) && z->encoded && *z->encoded) {
        apr_pool_userdata_get((void **)&driver, DRIVER_KEY,
                              r->server->process->pconf);
        res = decrypt_string(r, driver, dconf, z->encoded, &encoded);
        if (res != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, LOG_PREFIX
                    "decrypt session failed, wrong passphrase?");
            return res;
        }
        z->encoded = encoded;
    }

    return OK;

}

/**
 * Initialise the SSL in the post_config hook.
 */
AP_DECLARE(int) ap_session_crypto_init(apr_pool_t *p, apr_pool_t *plog,
        apr_pool_t *ptemp, server_rec *s)
{
    const apr_crypto_driver_t *driver = NULL;

    session_crypto_conf *conf = ap_get_module_config(s->module_config,
            &session_crypto_module);

    /* session_crypto_init() will be called twice. Don't bother
     * going through all of the initialization on the first call
     * because it will just be thrown away.*/
    if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG)
        return OK;

    if (conf->library) {

        const apu_err_t *err = NULL;
        apr_status_t rv;

        rv = apr_crypto_init(p, NULL);
        if (APR_SUCCESS != rv) {
            ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, LOG_PREFIX
                    "APR crypto could not be initialised");
            return rv;
        }

        rv = apr_crypto_get_driver(p, conf->library, &driver, conf->params, &err);
        if (APR_EREINIT == rv) {
            if (!conf->noinit) {
                ap_log_error(APLOG_MARK, APLOG_WARNING, rv, s, LOG_PREFIX
                        "warning: crypto for '%s' was already initialised, "
                        "using existing configuration", conf->library);
            }
            rv = APR_SUCCESS;
        }
        else {
            if (conf->noinit) {
                ap_log_error(APLOG_MARK, APLOG_WARNING, rv, s, LOG_PREFIX
                        "warning: crypto for '%s' was not previously initialised "
                        "when it was expected to be, initialised instead by "
                        "mod_session_crypto", conf->library);
            }
        }
        if (APR_SUCCESS != rv && err) {
            ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, LOG_PREFIX
                    "%s", err->msg);
            return rv;
        }
        if (APR_ENOTIMPL == rv) {
            ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, LOG_PREFIX
                    "The crypto library '%s' could not be found",
                    conf->library);
            return rv;
        }
        if (APR_SUCCESS != rv || !driver) {
            ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, LOG_PREFIX
                    "The crypto library '%s' could not be loaded",
                    conf->library);
            return rv;
        }

        ap_log_error(APLOG_MARK, APLOG_INFO, rv, s, LOG_PREFIX
                "The crypto library '%s' was loaded successfully",
                conf->library);

        apr_pool_userdata_set((const void *)driver, DRIVER_KEY,
                apr_pool_cleanup_null, s->process->pconf);

    }

    return OK;
}

static void *create_session_crypto_config(apr_pool_t * p, server_rec *s)
{
    session_crypto_conf *new =
    (session_crypto_conf *) apr_pcalloc(p, sizeof(session_crypto_conf));

    /* if no library has been configured, set the recommended library
     * as a sensible default.
     */
#ifdef APU_CRYPTO_RECOMMENDED_DRIVER
    new->library = APU_CRYPTO_RECOMMENDED_DRIVER;
#endif

    return (void *) new;
}

static void *create_session_crypto_dir_config(apr_pool_t * p, char *dummy)
{
    session_crypto_dir_conf *new =
    (session_crypto_dir_conf *) apr_pcalloc(p, sizeof(session_crypto_dir_conf));

    /* default cipher AES256-SHA */
    new->cipher = KEY_AES_256;

    return (void *) new;
}

static void *merge_session_crypto_dir_config(apr_pool_t * p, void *basev, void *addv)
{
    session_crypto_dir_conf *new = (session_crypto_dir_conf *) apr_pcalloc(p, sizeof(session_crypto_dir_conf));
    session_crypto_dir_conf *add = (session_crypto_dir_conf *) addv;
    session_crypto_dir_conf *base = (session_crypto_dir_conf *) basev;

    new->passphrase = (add->passphrase_set == 0) ? base->passphrase : add->passphrase;
    new->params = (add->passphrase_set == 0) ? base->params : add->params;
    new->passphrase_set = add->passphrase_set || base->passphrase_set;
    new->cipher = (add->cipher_set == 0) ? base->cipher : add->cipher;
    new->cipher_set = add->cipher_set || base->cipher_set;

    return new;
}

static const char *set_crypto_driver(cmd_parms * cmd, void *config, const char *arg)
{
    char *word, *val;
    int library_set = 0;
    session_crypto_conf *conf =
        (session_crypto_conf *)ap_get_module_config(cmd->server->module_config,
            &session_crypto_module);
    apr_crypto_param_t *param;

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);

    if (err != NULL) {
        return err;
    }

    conf->params = apr_array_make(cmd->pool, 10, sizeof(apr_crypto_param_t));

    while (*arg) {
        word = ap_getword_conf(cmd->pool, &arg);
        val = strchr(word, '=');
        if (!val) {
            if (!strcasecmp(word, "noinit")) {
                conf->noinit = 1;
                conf->noinit_set = 1;
            }
            else if (!library_set) {
                conf->library = word;
                conf->library_set = 1;
                library_set = 1;
            }
            else {
                return "Invalid SessionCryptoDriver parameter. Parameter must "
                "be in the form 'key=value'.";
            }
        }
        else {
            *val++ = '\0';
            if (!strcasecmp(word, "dir")) {
                param = apr_array_push(conf->params);
                param->type = APR_CRYPTO_CA_TYPE_DIR;
                param->path = val;
            }
            else if (!strcasecmp(word, "key3")) {
                param = apr_array_push(conf->params);
                param->type = APR_CRYPTO_CERT_TYPE_KEY3_DB;
                param->path = val;
            }
            else if (!strcasecmp(word, "cert7")) {
                param = apr_array_push(conf->params);
                param->type = APR_CRYPTO_CA_TYPE_CERT7_DB;
                param->path = val;
            }
            else if (!strcasecmp(word, "secmod")) {
                param = apr_array_push(conf->params);
                param->type = APR_CRYPTO_CA_TYPE_SECMOD;
                param->path = val;
            }
        }
    }

    return NULL;
}

static const char *set_crypto_passphrase(cmd_parms * cmd, void *config, const char *arg)
{
    char *word, *val;
    int passphrase_set = 0;
    session_crypto_dir_conf *dconf = (session_crypto_dir_conf *) config;
    apr_crypto_param_t *param;
    dconf->params = apr_array_make(cmd->pool, 10, sizeof(apr_crypto_param_t));

    while (*arg) {
        word = ap_getword_conf(cmd->pool, &arg);
        val = strchr(word, '=');
        if (!val) {
            if (!passphrase_set) {
                dconf->passphrase = word;
                dconf->passphrase_set = 1;
                passphrase_set = 1;
            }
            else {
                return "Invalid SessionCryptoPassphrase parameter. Parameter must "
                "be in the form 'key=value'.";
            }
        }
        else {
            *val++ = '\0';
            if (!strcasecmp(word, "engine")) {
                param = apr_array_push(dconf->params);
                param->type = APR_CRYPTO_ENGINE;
                param->path = val;
            }
            else if (!strcasecmp(word, "cipher")) {
                if (!strcasecmp(val, "3des192")) {
                    dconf->cipher = KEY_3DES_192;
                    dconf->cipher_set = 1;
                }
                else if (!strcasecmp(val, "aes256")) {
                    dconf->cipher = KEY_AES_256;
                    dconf->cipher_set = 1;
                }
                else {
                    return "Invalid SessionCryptoPassphrase parameter. Cipher must "
                    "be '3des192' or 'aes256'.";
                }
            }
            else {
                return "Invalid SessionCryptoPassphrase parameter. Parameters must "
                "be 'engine' or 'cipher'.";
            }
        }
    }

    return NULL;
}

static const command_rec session_crypto_cmds[] =
{
    AP_INIT_RAW_ARGS("SessionCryptoPassphrase", set_crypto_passphrase, NULL, RSRC_CONF|OR_AUTHCFG,
            "The passphrase used to encrypt the session"),
    AP_INIT_RAW_ARGS("SessionCryptoDriver", set_crypto_driver, NULL, RSRC_CONF,
            "The underlying crypto library driver to use"),
    {    NULL}
};

static void register_hooks(apr_pool_t * p)
{
    ap_hook_session_encode(ap_session_crypto_encode, NULL, NULL, APR_HOOK_LAST);
    ap_hook_session_decode(ap_session_crypto_decode, NULL, NULL, APR_HOOK_FIRST);
    ap_hook_post_config(ap_session_crypto_init, NULL, NULL, APR_HOOK_FIRST);
}

AP_DECLARE_MODULE(session_crypto) =
{
    STANDARD20_MODULE_STUFF,
    create_session_crypto_dir_config, /* dir config creater */
    merge_session_crypto_dir_config,  /* dir merger --- default is to
                                       * override */
    create_session_crypto_config,     /* server config */
    NULL,                             /* merge server config */
    session_crypto_cmds,              /* command apr_table_t */
    register_hooks                    /* register hooks */
};

#endif
