/*============================================================================
 *
 * CryptoCommon-c.inc
 *
 * DESCRIPTION
 *   Common C code for Filter::Crypto modules.
 *
 * COPYRIGHT
 *   Copyright (C) 2004-2009, 2012-2014, 2017 Steve Hay.  All rights reserved.
 *
 * LICENCE
 *   This file is free software; you can redistribute it and/or modify it under
 *   the same terms as Perl itself, i.e. under the terms of either the GNU
 *   General Public License or the Artistic License, as specified in the LICENCE
 *   file.
 *
 *============================================================================*/

/* Uncomment (or build with --debug-mode option) for debugging. */
/* #define FILTER_CRYPTO_DEBUG_MODE /**/

/* Uncomment for fixed (dummy) salt and/or IV for debugging purposes. */
/* #define FILTER_CRYPTO_NO_RAND_BYTES /**/

#include <memory.h>                     /* For memset().                      */
#include <stdarg.h>                     /* For va_list/va_start()/va_end().   */
#include <stdio.h>                      /* For snprintf().                    */
#include <stdlib.h>                     /* For atoi() and RAND_MAX.           */
#include <time.h>                       /* For time().                        */

                                        /* For getpid() and pid_t.            */
#ifdef WIN32
#  include <process.h>
#  ifndef __MINGW32__
     typedef int pid_t;
#  endif
#else
#  include <unistd.h>
#endif

/* For the ERR_*(), EVP_*() and RAND_*() functions. */
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>

#define PERL_NO_GET_CONTEXT             /* To get interp context efficiently. */
#define PERLIO_NOT_STDIO 0              /* To allow use of PerlIO and stdio.  */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#define NEED_sv_2pv_flags               /* For SvPV{X}_{nolen_}const().       */
#define NEED_PL_parser                  /* For PL_rsfp_filters.               */
#include "ppport.h"

#include "CipherConfig.h"               /* For the cipher details.            */

                                        /* RAND_MAX does not exist on SunOS.  */
#ifndef RAND_MAX
#  include <limits.h>
#  if INT_MAX == 32767
#    define RAND_MAX 32767
#  else
#    define RAND_MAX 0x7fffffff
#  endif
#endif

                                        /* snprintf() is an IEEE Std          */
                                        /* 1003.1-2001 extension to the ISO C */
                                        /* standard (ISO/IEC 9899:1999), and  */
                                        /* is called _snprintf() on Win32.    */
#ifdef WIN32
#  define snprintf _snprintf
#endif

#define FILTER_CRYPTO_OPENSSL_ERR_STR \
    (ERR_reason_error_string(FilterCrypto_GetLastSSLError()))

                                        /* Macro to set the CUR length in an  */
                                        /* SV whose string space has been     */
                                        /* manipulated directly.  Also adds a */
                                        /* NUL terminator in case the string  */
                                        /* gets used as a C "string" later.   */
#define FilterCrypto_SvSetCUR(sv, len) STMT_START { \
    if (SvPOK(sv)) {                                \
        char * const p = SvPVX(sv);                 \
        SvCUR_set(sv, len);                         \
        p[len] = '\0';                              \
    }                                               \
} STMT_END

#define FILTER_CRYPTO_NEED_RANDOM_SALT FILTER_CRYPTO_USING_PBE
#define FILTER_CRYPTO_NEED_RANDOM_IV   FILTER_CRYPTO_NEED_IV

                                        /* Convert between bytes and          */
                                        /* lowercase hexadecimal digits,      */
                                        /* assuming an ASCII character set.   */
#define B2H(b) (((b) >= 10) ? ('a' - 10 + (b)) : ('0' + (b)))
#define H2B(h) (((h) >= 'a') ? ((h) - 'a' + 10) : ((h) - '0'))

typedef enum {
    FILTER_CRYPTO_MODE_DECRYPT = 0,
    FILTER_CRYPTO_MODE_ENCRYPT
} FILTER_CRYPTO_MODE;

typedef struct {
    EVP_CIPHER_CTX *cipher_ctx;
    SV *salt_sv;
    int required_salt_len;
    SV *iv_sv;
    int required_iv_len;
    FILTER_CRYPTO_MODE crypt_mode;
    bool cipher_initialized;
} FILTER_CRYPTO_CCTX;

static FILTER_CRYPTO_CCTX *FilterCrypto_CryptoAlloc(pTHX);
static bool FilterCrypto_CryptoInit(pTHX_ FILTER_CRYPTO_CCTX *ctx,
    FILTER_CRYPTO_MODE crypt_mode);
static bool FilterCrypto_CryptoInitCipher(pTHX_ FILTER_CRYPTO_CCTX *ctx,
    SV *in_sv, SV *out_sv);
static bool FilterCrypto_CryptoUpdate(pTHX_ FILTER_CRYPTO_CCTX *ctx, SV *in_sv,
    SV *out_sv);
static bool FilterCrypto_CryptoFinal(pTHX_ FILTER_CRYPTO_CCTX *ctx, SV *out_sv);
static void FilterCrypto_CryptoFree(pTHX_ FILTER_CRYPTO_CCTX *ctx);
static bool FilterCrypto_CipherInit(pTHX_ EVP_CIPHER_CTX *ctx, SV *salt_sv,
    SV *iv_sv, FILTER_CRYPTO_MODE crypt_mode);
static bool FilterCrypto_CipherUpdate(pTHX_ EVP_CIPHER_CTX *ctx, SV *in_sv,
    SV *out_sv);
static bool FilterCrypto_CipherFinal(pTHX_ EVP_CIPHER_CTX *ctx, SV *out_sv);
static bool FilterCrypto_PRNGInit(pTHX);
static int FilterCrypto_GetRandNum(pTHX_ int min, int max);
static unsigned long FilterCrypto_GetLastSSLError(void);
static void FilterCrypto_SetErrStr(pTHX_ const char *value, ...);
static void FilterCrypto_EncodeSV(pTHX_ const SV *in_sv, SV *out_sv);
static bool FilterCrypto_DecodeSV(pTHX_ const SV *in_sv, SV *out_sv);
#ifdef FILTER_CRYPTO_DEBUG_MODE
static void FilterCrypto_vHexDump(pTHX_ const unsigned char *data,
    unsigned int len, const char *title, va_list args);
static void FilterCrypto_HexDump(pTHX_ const unsigned char *data,
    unsigned int len, const char *title, ...);
static void FilterCrypto_HexDumpSV(pTHX_ const SV *sv, const char *title, ...);
#endif

/* Fully-qualified name of the relevant Perl module's $ErrStr variable.  This is
 * set at boot time from the (hard-coded) unqualified name and the package name
 * #defined in the individual XS files and is not subsequently changed, so is
 * virtually a "const" and is therefore thread-safe. */
static char *filter_crypto_errstr_var = NULL;

/*
 * Function to allocate a new crypto context.
 * Returns a pointer to the allocated structure.
 */

static FILTER_CRYPTO_CCTX *FilterCrypto_CryptoAlloc(pTHX) {
    FILTER_CRYPTO_CCTX *ctx;

    /* Allocate the new crypto context. */
    Newxz(ctx, 1, FILTER_CRYPTO_CCTX);

    /* Allocate the cipher context. */
#if FILTER_CRYPTO_OPENSSL_VERSION < 90802
    Newxz(ctx->cipher_ctx, 1, EVP_CIPHER_CTX);
#else
    ctx->cipher_ctx = EVP_CIPHER_CTX_new();
#endif

    /* Allocate a pair of SVs with enough string space to hold a salt and an
     * initialization vector (IV) respectively, and store their required
     * lengths.  Mark each one as being a string only. */
#if FILTER_CRYPTO_NEED_RANDOM_SALT
    ctx->salt_sv = newSV(PKCS5_SALT_LEN);
    SvPOK_only(ctx->salt_sv);
    ctx->required_salt_len = PKCS5_SALT_LEN;
#endif

#if FILTER_CRYPTO_NEED_RANDOM_IV
    ctx->iv_sv = newSV(EVP_CIPHER_iv_length(FILTER_CRYPTO_CIPHER_FUNC));
    SvPOK_only(ctx->iv_sv);
    ctx->required_iv_len = EVP_CIPHER_iv_length(FILTER_CRYPTO_CIPHER_FUNC);
#endif

    return ctx;
}

/*
 * Function to initialize the given crypto context in the given mode.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CryptoInit(pTHX_ FILTER_CRYPTO_CCTX *ctx,
    FILTER_CRYPTO_MODE crypt_mode)
{
    /* The cipher context gets initialized later (by
     * FilterCrypto_CryptoInitCipher(), called from FilterCrypto_CryptoUpdate())
     * rather than now because we do not necessarily have all the required
     * information to do it now. */

    /* Initialize the salt and IV. */
#if FILTER_CRYPTO_NEED_RANDOM_SALT
    FilterCrypto_SvSetCUR(ctx->salt_sv, 0);
#else
    ctx->salt_sv = (SV *)NULL;
#endif
#if FILTER_CRYPTO_NEED_RANDOM_IV
    FilterCrypto_SvSetCUR(ctx->iv_sv,   0);
#else
    ctx->iv_sv = (SV *)NULL;
#endif

    /* Initialize the crypt mode and cipher context initialization status. */
    ctx->crypt_mode         = crypt_mode;
    ctx->cipher_initialized = FALSE;

    /* Clear the current thread's OpenSSL/SSLeay error queue. */
    ERR_clear_error();

    /* Clear our own last error message. */
    FilterCrypto_SetErrStr(aTHX_ "");
    return TRUE;
}

/*
 * Function to initialize the cipher context within the given crypto context.
 * This is called automatically from FilterCrypto_CryptoUpdate() since when
 * decrypting, a salt and/or an IV may need to be recovered from the given input
 * SV that is passed to FilterCrypto_CryptoUpdate().  In that case, the salt
 * and/or IV will be removed from the start of the input SV, leaving any further
 * data in place for FilterCrypto_CryptoUpdate() to process afterwards.  When
 * encrypting, it may be necessary to have a salt and/or IV randomly generated.
 * In that case, they will be written to the start of the given output SV, which
 * will be grown accordingly to accomodate them as well as the output data that
 * it should already be large enough for.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CryptoInitCipher(pTHX_ FILTER_CRYPTO_CCTX *ctx,
    SV *in_sv, SV *out_sv)
{
    /* If we're using password-based encryption (PBE) then a salt is required
     * for the key derivation.  It must be randomly generated when encrypting,
     * and output with the encrypted data so that it can be read back in and
     * used again for decrypting.  We also need an IV, which must be handled in
     * the same way. */
#if (FILTER_CRYPTO_NEED_RANDOM_SALT || FILTER_CRYPTO_NEED_RANDOM_IV)
    switch (ctx->crypt_mode) {
        case FILTER_CRYPTO_MODE_ENCRYPT: {
#  if FILTER_CRYPTO_NEED_RANDOM_SALT
            unsigned char *salt_text = (unsigned char *)SvPVX(ctx->salt_sv);
#  endif
#  if FILTER_CRYPTO_NEED_RANDOM_IV
            unsigned char *iv_text   = (unsigned char *)SvPVX(ctx->iv_sv);
#  endif

            /* Ensure the pseudo-random number generator (PRNG) is seeded. */
            if (!FilterCrypto_PRNGInit(aTHX))
                return FALSE;

#  if FILTER_CRYPTO_NEED_RANDOM_SALT
#    if defined(FILTER_CRYPTO_NO_RAND_BYTES)
            memset(salt_text, '*', ctx->required_salt_len);
#    else
            if (!RAND_bytes(salt_text, ctx->required_salt_len)) {
                if (!RAND_pseudo_bytes(salt_text, ctx->required_salt_len)) {
                    FilterCrypto_SetErrStr(aTHX_
                        "Can't generate %d-byte random salt: %s",
                        ctx->required_salt_len, FILTER_CRYPTO_OPENSSL_ERR_STR
                    );
                    return FALSE;
                }
                warn("Random salt may not be cryptographically strong");
            }
#    endif
            FilterCrypto_SvSetCUR(ctx->salt_sv, ctx->required_salt_len);

            /* Grow the output SV's buffer enough to hold the salt as well as
             * the output data that it should already be large enough for. */
            SvGROW(out_sv, SvLEN(out_sv) + ctx->required_salt_len);

            sv_catpvn(out_sv, salt_text, ctx->required_salt_len);

#    ifdef FILTER_CRYPTO_DEBUG_MODE
            FilterCrypto_HexDump(aTHX_ salt_text, ctx->required_salt_len,
                "Generated %d-byte salt", ctx->required_salt_len
            );
#    endif
#  endif

#  if FILTER_CRYPTO_NEED_RANDOM_IV
#    if defined(FILTER_CRYPTO_NO_RAND_BYTES)
            memset(iv_text, '*', ctx->required_iv_len);
#    else
            if (!RAND_bytes(iv_text, ctx->required_iv_len)) {
                if (!RAND_pseudo_bytes(iv_text, ctx->required_iv_len)) {
                    FilterCrypto_SetErrStr(aTHX_
                        "Can't generate %d-byte random IV: %s",
                        ctx->required_iv_len, FILTER_CRYPTO_OPENSSL_ERR_STR
                    );
                    return FALSE;
                }
                warn("Random IV may not be cryptographically strong");
            }
#    endif
            FilterCrypto_SvSetCUR(ctx->iv_sv, ctx->required_iv_len);

            /* Grow the output SV's buffer enough to hold the IV as well as the
             * output data that it should already be large enough for. */
            SvGROW(out_sv, SvLEN(out_sv) + ctx->required_iv_len);

            sv_catpvn(out_sv, iv_text, ctx->required_iv_len);

#    ifdef FILTER_CRYPTO_DEBUG_MODE
            FilterCrypto_HexDump(aTHX_ iv_text, ctx->required_iv_len,
                "Generated %d-byte IV", ctx->required_iv_len
            );
#    endif
#  endif

            break;
        }

        case FILTER_CRYPTO_MODE_DECRYPT: {
#  if FILTER_CRYPTO_NEED_RANDOM_SALT
            int missing_salt_len;
#  endif
#  if FILTER_CRYPTO_NEED_RANDOM_IV
            int missing_iv_len;
#  endif
            int in_len;
            const unsigned char *in_text;

#  if FILTER_CRYPTO_NEED_RANDOM_SALT
            missing_salt_len = ctx->required_salt_len - SvCUR(ctx->salt_sv);
            if (missing_salt_len > 0) {
                in_len = SvCUR(in_sv);
                in_text = (const unsigned char *)SvPVX_const(in_sv);
                if (missing_salt_len <= in_len) {
                    sv_catpvn(ctx->salt_sv, in_text, missing_salt_len);
                    sv_chop(in_sv, (char *)in_text + missing_salt_len);
                }
                else {
                    sv_catpvn(ctx->salt_sv, in_text, in_len);
                    FilterCrypto_SvSetCUR(in_sv, 0);

                    /* We have not fully populated the salt yet so just return
                     * success for now.  We'll be called again later. */
                    return TRUE;
                }

#    ifdef FILTER_CRYPTO_DEBUG_MODE
                FilterCrypto_HexDumpSV(aTHX_ ctx->salt_sv,
                    "Recovered %d-byte salt", SvCUR(ctx->salt_sv)
                );
#    endif
            }
#  endif

#  if FILTER_CRYPTO_NEED_RANDOM_IV
            missing_iv_len = ctx->required_iv_len - SvCUR(ctx->iv_sv);
            if (missing_iv_len > 0) {
                in_len = SvCUR(in_sv);
                in_text = (const unsigned char *)SvPVX_const(in_sv);
                if (missing_iv_len <= in_len) {
                    sv_catpvn(ctx->iv_sv, in_text, missing_iv_len);
                    sv_chop(in_sv, (char *)in_text + missing_iv_len);
                }
                else {
                    sv_catpvn(ctx->iv_sv, in_text, in_len);
                    FilterCrypto_SvSetCUR(in_sv, 0);

                    /* We have not fully populated the IV yet so just return
                     * success for now.  We'll be called again later. */
                    return TRUE;
                }

#    ifdef FILTER_CRYPTO_DEBUG_MODE
                FilterCrypto_HexDumpSV(aTHX_ ctx->iv_sv,
                    "Recovered %d-byte IV", SvCUR(ctx->iv_sv)
                );
#    endif
            }
#  endif

            /* Make sure the in_sv's OOK flag is turned off in case it was set
             * by the use of sv_chop() above.  This copies any bytes remaining
             * in in_sv's PV slot to the start of that slot so that they are
             * still available for FilterCrypto_CryptoUpdate(). */
            SvOOK_off(in_sv);

            break;
        }

        default:
            croak("Unknown crypto context mode '%d'", ctx->crypt_mode);
    }
#endif

    if (!FilterCrypto_CipherInit(aTHX_ ctx->cipher_ctx, ctx->salt_sv,
            ctx->iv_sv, ctx->crypt_mode))
        return FALSE;

    ctx->cipher_initialized = TRUE;

    return TRUE;
}

/*
 * Function to update the given crypto context with data in the given input SV.
 * This data is not assumed to be null-terminated, so the correct length must be
 * set in SvCUR(in_sv).  Likewise for the data written into the output SV:
 * SvCUR(out_sv) will be set correctly by this function.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CryptoUpdate(pTHX_ FILTER_CRYPTO_CCTX *ctx, SV *in_sv,
    SV *out_sv)
{
    /* If the cipher context is not yet initialized then use the data in the
     * input SV to initialize it now. */
    if (!ctx->cipher_initialized) {
        if (!FilterCrypto_CryptoInitCipher(aTHX_ ctx, in_sv, out_sv))
            return FALSE;
    }

    /* If there is any data left in the input SV after the above initialization
     * has potentially been run then use it to update the cipher context.
     * Otherwise just return success. */
    if (SvCUR(in_sv))
        return FilterCrypto_CipherUpdate(aTHX_ ctx->cipher_ctx, in_sv, out_sv);
    else
        return TRUE;
}

/*
 * Function to finalize the given crypto context.  The data written into the
 * output SV is not assumed to be null-terminated, so SvCUR(out_sv) will be set
 * correctly by this function.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CryptoFinal(pTHX_ FILTER_CRYPTO_CCTX *ctx, SV *out_sv)
{
    if (ctx->cipher_initialized == TRUE)
        return FilterCrypto_CipherFinal(aTHX_ ctx->cipher_ctx, out_sv);
    else
        return TRUE;
}

/*
 * Function to free the given crypto context.
 */

static void FilterCrypto_CryptoFree(pTHX_ FILTER_CRYPTO_CCTX *ctx) {
    /* Free the IV and salt by decrementing their reference counts (to zero). */
#if FILTER_CRYPTO_NEED_RANDOM_IV
    SvREFCNT_dec(ctx->iv_sv);
#endif
#if FILTER_CRYPTO_NEED_RANDOM_SALT
    SvREFCNT_dec(ctx->salt_sv);
#endif

    /* Free the cipher context. */
#if FILTER_CRYPTO_OPENSSL_VERSION < 90802
    Safefree(ctx->cipher_ctx);
#else
    EVP_CIPHER_CTX_free(ctx->cipher_ctx);
#endif
    ctx->cipher_ctx = NULL;

    /* Free the crypto context. */
    Safefree(ctx);
    ctx = NULL;
}

/*
 * Function to initialize the given cipher context in the given mode using the
 * given salt and/or IV if necessary.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CipherInit(pTHX_ EVP_CIPHER_CTX *ctx, SV *salt_sv,
    SV *iv_sv, FILTER_CRYPTO_MODE crypt_mode)
{
    const EVP_CIPHER *cipher_func = FILTER_CRYPTO_CIPHER_FUNC;
#if FILTER_CRYPTO_KEY_LEN == 0
    unsigned char *key = NULL;
#else
    unsigned char key[FILTER_CRYPTO_KEY_LEN];
#endif
#if FILTER_CRYPTO_NEED_RANDOM_SALT
    unsigned char *salt = (unsigned char *)SvPVX(salt_sv);
    int salt_len = SvCUR(salt_sv);
#endif
#if FILTER_CRYPTO_NEED_RANDOM_IV
    unsigned char *iv = (unsigned char *)SvPVX(iv_sv);
#else
    unsigned char *iv = NULL;
#endif

    /* Derive the key from the given password.  PBE should really be initialized
     * with PKCS5_pbe2_set() and EVP_PBE_CipherInit().  The former generates a
     * random IV, while the latter derives a key from the given password by a
     * PKCS#5 v2.0 key derivation algorithm (via PKCS5_v2_PBE_keyivgen() and
     * ultimately PKCS5_PBKDF2_HMAC_SHA1()).  There is currently (as of 0.9.7e)
     * a problem with PKCS5_pbe2_set() in that the IV cannot be user-specified,
     * but this could be overcome by DER-encoding the X509_ALGOR structure
     * returned by it and writing/reading this when encrypting/decrypting as we
     * currently do with the salt and/or IV.  However, there is another problem,
     * with PKCS5_v2_PBE_keyivgen(), which cannot be overcome so easily: it only
     * works with the default key length of the given cipher, so we would lose
     * the ability to set the key length differently for those algorithms with
     * variable key lengths.  There are also problems using EVP_PBE_CipherInit()
     * at all with some ciphers because it relies on the ASN1 code, which, as
     * the "BUGS" section of the EVP_EncryptInit.pod in recent OpenSSL
     * distributions says, is incomplete and sometimes inaccurate.  Therefore,
     * we use the standard EVP_CipherInit[_ex]() functions, and call
     * PKCS5_PBKDF2_HMAC_SHA1() directly ourselves to do the PKCS#5 v2.0 key
     * derivation.
     * See the exchanges between myself and Steve Henson on the "openssl-users"
     * mailing list, 08-13 Sep 2004, for more details on all of this. */

    /* The EVP library API has facilities for modifying the key length for
     * variable key length ciphers and for modifying other cipher parameters, so
     * to start with we just specify which cipher we are using.  Then we can set
     * the key length and other parameters in the cipher context structure thus
     * created, and then finally derive the key and set both it and the IV in
     * the cipher context structure. */
#  if FILTER_CRYPTO_OPENSSL_VERSION < 90700
    if (!EVP_CipherInit(ctx, cipher_func, NULL, NULL, crypt_mode)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't initialize cipher context in crypt mode '%d': %s",
            crypt_mode, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  else
    EVP_CIPHER_CTX_init(ctx);

    if (!EVP_CipherInit_ex(ctx, cipher_func, NULL, NULL, NULL, crypt_mode)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't initialize cipher context in crypt mode '%d': %s",
            crypt_mode, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  endif

    /* Now we can modify the parameters for the chosen cipher.  First, set the
     * key length. */
    if (!EVP_CIPHER_CTX_set_key_length(ctx, FILTER_CRYPTO_KEY_LEN)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't set key length to %d: %s",
            FILTER_CRYPTO_KEY_LEN, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }

    /* Now modify any other cipher-specific parameters that we have. */
#  if defined(FILTER_CRYPTO_RC2_KEY_BITS)
    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_SET_RC2_KEY_BITS,
            FILTER_CRYPTO_RC2_KEY_BITS, NULL))
    {
        FilterCrypto_SetErrStr(aTHX_
            "Can't set RC2 effective key bits to %d: %s",
            FILTER_CRYPTO_RC2_KEY_BITS, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  elif defined(FILTER_CRYPTO_RC5_ROUNDS)
    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_SET_RC5_ROUNDS,
            FILTER_CRYPTO_RC5_ROUNDS, NULL))
    {
        FilterCrypto_SetErrStr(aTHX_
            "Can't set RC5 number of rounds to %d: %s",
            FILTER_CRYPTO_RC5_ROUNDS, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  endif

    /* We have finished modifying the cipher parameters, so we can now
     * finish the initialization of the cipher context by deriving the key
     * and setting both it and the IV. */
#  if FILTER_CRYPTO_USING_PBE
    if (PKCS5_PBKDF2_HMAC_SHA1(filter_crypto_pswd, sizeof(filter_crypto_pswd),
            salt, salt_len, PKCS5_DEFAULT_ITER, FILTER_CRYPTO_KEY_LEN, key) !=
            1)
    {
        FilterCrypto_SetErrStr(aTHX_
            "Can't derive %d-byte key: %s",
            FILTER_CRYPTO_KEY_LEN, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  else
    Copy(filter_crypto_key, key, FILTER_CRYPTO_KEY_LEN, unsigned char);
#  endif

#  if FILTER_CRYPTO_OPENSSL_VERSION < 90700
    if (!EVP_CipherInit(ctx, NULL, key, iv, crypt_mode)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't initialize cipher context in crypt mode '%d' using %d-byte "
            "key: %s",
            crypt_mode, FILTER_CRYPTO_KEY_LEN, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  else
    if (!EVP_CipherInit_ex(ctx, NULL, NULL, key, iv, crypt_mode)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't initialize cipher context in crypt mode '%d' using %d-byte "
            "key: %s",
            crypt_mode, FILTER_CRYPTO_KEY_LEN, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#  endif

    /* Wipe the key from memory now that it has been set in the cipher context.
     * It's still around somewhere, of course, but at least this is one place
     * less that it might be found. */
    Poison(key, FILTER_CRYPTO_KEY_LEN, unsigned char);

    return TRUE;
}

/*
 * Function to update the given cipher context with the data in the given input
 * SV.  This data is not assumed to be null-terminated, so the correct length
 * must be set in SvCUR(in_sv).  Likewise for the data written into the output
 * SV: SvCUR(out_sv) will be set correctly by this function.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CipherUpdate(pTHX_ EVP_CIPHER_CTX *ctx, SV *in_sv,
    SV *out_sv)
{
#if FILTER_CRYPTO_OPENSSL_VERSION < 90700
    unsigned char *in_text = (unsigned char *)SvPVX(in_sv);
#else
    const unsigned char *in_text = (const unsigned char *)SvPVX_const(in_sv);
#endif
    unsigned char *out_text;
    int in_len = SvCUR(in_sv);
    int orig_out_len;
    int out_len;

    /* Up to in_len + EVP_CIPHER_CTX_block_size(ctx) - 1 bytes may be written
     * when encrypting, and up to in_len + EVP_CIPHER_CTX_block_size(ctx) bytes
     * may be written when decrypting, so ensure that the output buffer is big
     * enough (plus space for a NUL terminator). */
    out_text = (unsigned char *)SvGROW(
        out_sv, (STRLEN)(in_len + EVP_CIPHER_CTX_block_size(ctx) + 1)
    );

    /* Advance the out_text pointer to the end of any existing output since the
     * output buffer may already have a salt and/or an IV in it if we have just
     * initialized an encryption process. */
    orig_out_len = SvCUR(out_sv);
    out_text += orig_out_len;

    if (!EVP_CipherUpdate(ctx, out_text, &out_len, in_text, in_len)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't update cipher context with %d bytes of in-text: %s",
            in_len, FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }

#ifdef FILTER_CRYPTO_DEBUG_MODE
    FilterCrypto_HexDump(aTHX_ out_text, out_len,
        "Converted %d bytes to %d bytes", in_len, out_len
    );
#endif

    /* Set the output length in the output SV, again accounting for any output
     * that already existed. */
    FilterCrypto_SvSetCUR(out_sv, orig_out_len + out_len);

    return TRUE;
}

/*
 * Function to finalize the given cipher context.  The data written into the
 * output SV is not assumed to be null-terminated, so SvCUR(out_sv) will be set
 * correctly by this function.
 * Returns a bool to indicate success or failure.
 */

static bool FilterCrypto_CipherFinal(pTHX_ EVP_CIPHER_CTX *ctx, SV *out_sv) {
    unsigned char *out_text;
    int out_len;

    /* Up to EVP_CIPHER_CTX_block_size(ctx) bytes may be written when encrypting
     * or decrypting, so ensure that the output buffer is big enough (plus space
     * for a NUL terminator). */
    out_text = (unsigned char *)SvGROW(
        out_sv, (STRLEN)(EVP_CIPHER_CTX_block_size(ctx) + 1)
    );

#if FILTER_CRYPTO_OPENSSL_VERSION < 90700
    if (!EVP_CipherFinal(ctx, out_text, &out_len)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't finalize cipher context: %s", FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#else
    if (!EVP_CipherFinal_ex(ctx, out_text, &out_len)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't finalize cipher context: %s", FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }

    if (!EVP_CIPHER_CTX_cleanup(ctx)) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't cleanup cipher context: %s", FILTER_CRYPTO_OPENSSL_ERR_STR
        );
        return FALSE;
    }
#endif

#ifdef FILTER_CRYPTO_DEBUG_MODE
    FilterCrypto_HexDump(aTHX_ out_text, out_len,
        "Converted final block to %d bytes", out_len
    );
#endif

    /* Set the output length in the output SV. */
    FilterCrypto_SvSetCUR(out_sv, out_len);

    return TRUE;
}

/*
 * Function to initialize the OpenSSL PRNG.
 * Returns a bool to indicate success or failure.
 *
 * This function is based on code taken from the ssl_rand_seed() function in
 * Apache httpd (version 2.4.9).
 */

static bool FilterCrypto_PRNGInit(pTHX) {
    /* The PRNG is seeded transparently for us on some OSes (e.g. using UNIX's
     * /dev/urandom or /dev/random devices or Win32's Crypto API) in some
     * versions of OpenSSL and SSLeay, but in other cases we must do it
     * manually.  RAND_status() reports whether the PRNG is seeded. */
    if (RAND_status())
        return TRUE;

    /* Win32 had a RAND_screen() function (deprecated in OpenSSL 1.1.0) for
     * seeding the PRNG.  In other cases we just have to manage ourselves. */
#if (defined(WIN32) && FILTER_CRYPTO_OPENSSL_VERSION < 101000)
    RAND_screen();
#else
    {
        struct {
            time_t t;
            pid_t pid;
        } my_seed;
        int n;
        unsigned char stackdata[256];

        my_seed.t = time(NULL);
        my_seed.pid = getpid();
        RAND_seed((unsigned char *)&my_seed, sizeof(my_seed));

        n = FilterCrypto_GetRandNum(aTHX_ 0, sizeof(stackdata) - 128 - 1);
        RAND_seed(stackdata + n, 128);
    }
#endif

    /* Check whether we have seeded the PRNG with enough entropy. */
    if (RAND_status()) {
        return TRUE;
    }
    else {
        FilterCrypto_SetErrStr(aTHX_ "Can't initialize PRNG");
        return FALSE;
    }
}

/*
 * Function to return a random number between min and max.
 *
 * This function is based on the ssl_rand_choosenum() function in Apache httpd
 * (version 2.4.9).
 */

static int FilterCrypto_GetRandNum(pTHX_ int min, int max) {
    char buf[50];
    int n;

    seedDrand01((Rand_seed_t)time(NULL));
    PL_srand_called = TRUE;

    snprintf(buf, sizeof(buf), "%.0f", Drand01() * (max - min));

    n = atoi(buf) + 1;

    if (n < min)
        n = min;

    if (n > max)
        n = max;

    return n;
}

/*
 * Function to get the last (most recent) OpenSSL error from the current
 * thread's error queue.
 */

static unsigned long FilterCrypto_GetLastSSLError(void) {
#if FILTER_CRYPTO_OPENSSL_VERSION >= 90700
    return ERR_peek_last_error();
#else
    unsigned long err;
    unsigned long last_err = 0;
    /* There are no ERR_peek_last_*() functions before 0.9.7, so we have to get
     * the errors (removing them from the queue) from the earliest to the last
     * instead.  We probably ought to put them back again afterwards, but it
     * does not really matter. */
    while ((err = ERR_get_error()))
        last_err = err;
    return last_err;
#endif
}

/*
 * Function to set the relevant Perl module's $ErrStr variable to the given
 * value.
 */

static void FilterCrypto_SetErrStr(pTHX_ const char *value, ...) {
    va_list args;

    /* Get the relevant Perl module's $ErrStr variable and set an appropriate
     * value in it. */
    va_start(args, value);
    sv_vsetpvf(get_sv(filter_crypto_errstr_var, TRUE), value, &args);
    va_end(args);
}

/*
 * Function to encode the text from one SV into another SV.  Each byte is
 * encoded as a pair of hexadecimal digits.
 */

static void FilterCrypto_EncodeSV(pTHX_ const SV *in_sv, SV *out_sv)
{
    const unsigned char *in_text;
    unsigned char *out_text;
    STRLEN in_len;
    STRLEN out_len;
    unsigned int i;

    /* Clear the output SV before encoding into it. */
    FilterCrypto_SvSetCUR(out_sv, 0);

    in_text = (const unsigned char *)SvPVX_const(in_sv);
    out_text = (unsigned char *)SvPVX(out_sv);
    in_len = SvCUR(in_sv);
    out_len = SvCUR(out_sv);

    for (i = 0; i < in_len; i++) {
        out_text[2 * i]     = B2H((in_text[i] & 0xf0) >> 4);
        out_text[2 * i + 1] = B2H( in_text[i] & 0x0f);
        out_len += 2;
    }

#ifdef FILTER_CRYPTO_DEBUG_MODE
    FilterCrypto_HexDump(aTHX_ out_text, out_len,
        "Encoded %d bytes to %d bytes", in_len, out_len
    );
#endif

    /* Set the output length in the output SV. */
    FilterCrypto_SvSetCUR(out_sv, out_len);
}

/*
 * Function to decode the text from one SV into another SV.  Inverse function
 * of FilterCrypto_EncodeSV().
 */

static bool FilterCrypto_DecodeSV(pTHX_ const SV *in_sv, SV *out_sv) {
    const unsigned char *in_text;
    unsigned char *out_text;
    STRLEN in_len;
    STRLEN out_len;
    unsigned int i;

    /* Clear the output SV before decoding into it. */
    FilterCrypto_SvSetCUR(out_sv, 0);

    in_text = (const unsigned char *)SvPVX_const(in_sv);
    out_text = (unsigned char *)SvPVX(out_sv);
    in_len = SvCUR(in_sv);
    out_len = SvCUR(out_sv);

    if (in_len % 2) {
        FilterCrypto_SetErrStr(aTHX_
            "Can't decode odd-numbered (%d-byte) length hexadecimal text",
            in_len
        );
        return FALSE;
    }

    for (i = 0; i < in_len; i++) {
        if (!isXDIGIT(in_text[i])) {
            FilterCrypto_SetErrStr(aTHX_
                "Can't decode non-hexadecimal digit (byte %02x at position %d) "
                "in hexadecimal text", in_text[i], i + 1
            );
            return FALSE;
        }
    }

    for (i = 0; i < in_len; i += 2) {
        out_text[i/2] = (H2B(in_text[i]) << 4) | H2B(in_text[i + 1]);
        out_len++;
    }

#ifdef FILTER_CRYPTO_DEBUG_MODE
    FilterCrypto_HexDump(aTHX_ out_text, out_len,
        "Decoded %d bytes to %d bytes", in_len, out_len
    );
#endif

    /* Set the output length in the output SV. */
    FilterCrypto_SvSetCUR(out_sv, out_len);

    return TRUE;
}

#ifdef FILTER_CRYPTO_DEBUG_MODE
/*
 * Function to print a dump of the given data.  Each character (octet) is shown
 * as itself if it is "printable"; otherwise it is shown as its hexadecimal
 * code.  An optional title can be printed first.  Pass NULL as the third
 * argument to omit the title.
 * Use this via either FilterCrypto_HexDump() or FilterCrypto_HexDumpSV().
 */

static void FilterCrypto_vHexDump(pTHX_ const unsigned char *data,
    unsigned int len, const char *title, va_list args)
{
    unsigned int i;

    if (title) {
        PerlIO_vprintf(PerlIO_stderr(), title, args);
        PerlIO_printf(PerlIO_stderr(), ":\n");
    }

    for (i = 0; i < len; ++i) {
        if (i % 16 == 0)
            PerlIO_printf(PerlIO_stderr(), "%s%08x", i == 0 ? "" : "\n", i);

        if (data[i] >= 32 && data[i] <= 127)
            PerlIO_printf(PerlIO_stderr(), " %2c", data[i]);
        else
            PerlIO_printf(PerlIO_stderr(), " %02x", data[i]);
    }

    PerlIO_printf(PerlIO_stderr(), "%s%08x\n", i == 0 ? "" : "\n", i);
}

/*
 * Function to print a dump of the given data.
 */

static void FilterCrypto_HexDump(pTHX_ const unsigned char *data,
    unsigned int len, const char *title, ...)
{
    va_list args;

    va_start(args, title);
    FilterCrypto_vHexDump(aTHX_ data, len, title, args);
    va_end(args);
}

/*
 * Function to print a dump of the data in the given SV.
 */

static void FilterCrypto_HexDumpSV(pTHX_ const SV *sv, const char *title, ...) {
    STRLEN len;
    const unsigned char *data;
    va_list args;

    data = (const unsigned char *)SvPVX_const(sv);
    len = SvCUR(sv);
    va_start(args, title);
    FilterCrypto_vHexDump(aTHX_ data, len, title, args);
    va_end(args);
}
#endif

/*============================================================================*/
