#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#define NEED_mg_findext
#include "ppport.h"
#include "cmk.h"

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/crypto.h>
#include <openssl/kdf.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/objects.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
  #include <openssl/decoder.h>
  #include <openssl/core_names.h>
  #include <openssl/params.h>
#else
  /* 1.1.1 needs EC headers */
  #include <openssl/ec.h>
  #include <openssl/hmac.h>
#endif

#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#define CMK_AES_256_XTS_KEY_LEN 64
#define CMK_AES_256_GCM_KEY_LEN 32
#define CMK_AES_256_GCM_NONCE_LEN 12
#define CMK_AES_256_GCM_TAG_LEN 16
#define CMK_PBKDF2_SALT_LEN 16
#define CMK_KDF_SALT_LEN 32
#define CMK_RSA_KEYMATERIAL_LEN 32

extern MAGIC* cmk_get_X_magic(pTHX_ SV *obj, int flags, const MGVTBL *mg_vtbl, const char *mg_desc) {
   SV *sv;
   MAGIC *magic;

   if ((!obj || !SvOK(obj)) && (flags & CMK_MAGIC_UNDEF_OK))
      return NULL;

   if (!sv_isobject(obj)) {
      if (flags & CMK_MAGIC_OR_DIE)
         croak("Not an object");
   }
   sv = SvRV(obj);
   if (SvMAGICAL(sv) && (magic = mg_findext(sv, PERL_MAGIC_ext, mg_vtbl)))
      return magic;

   if (flags & CMK_MAGIC_AUTOCREATE) {
      magic = sv_magicext(sv, NULL, PERL_MAGIC_ext, mg_vtbl, NULL, 0);
#ifdef USE_ITHREADS
      magic->mg_flags |= MGf_DUP;
#endif
      return magic;
   }
   if (flags & CMK_MAGIC_OR_DIE)
      croak("Object lacks '%s' magic", mg_desc);
   return NULL;
}

/* I am only using foldEQ as a cross-platform stricmp for parsing user parameters,
 * so this should be fine for Perl < 5.14 */
#ifndef foldEQ
static bool shim_foldEQ(const char *s1, const char *s2, int len) {
        for (--len; len >= 0; --len)
                if (toLOWER(s1[len]) != toLOWER(s2[len]))
                        return 0;
        return 1;
}
#define foldEQ shim_foldEQ
#endif

/* Force a SV to be a non-shared buffer of bytes of the requested size.
 * This changes the SV length even though the bytes haven't been initialized yet.
 * It also initializes the NUL-terminator.
 */
static char* cmk_prepare_sv_buffer(SV *sv, size_t size) {
   char *p;
   if (!SvOK(sv)) /* prevent "Use of uninitialized value in subroutine entry" */
      sv_setpvs(sv, "");
   p= sv_grow(sv, size+1);
   SvCUR_set(sv, size);
   p[size]= '\0';
   return p;
}

static bool looks_like_integer(SV *sv) {
   return SvNOK(sv)? SvNV(sv) == SvIV(sv)
        : SvIOK(sv) || SvUOK(sv)? true
        : looks_like_number(sv)? SvNV(sv) == SvIV(sv)
        : false;
}

static size_t round_up_to_pow2(size_t n) {
   --n;
   n |= n >> 1;
   n |= n >> 2;
   n |= n >> 4;
   n |= n >> 8;
   n |= n >> 16;
   n |= n >> 32;
   return n+1;
}

/* Generate a version 4 (random) UUID into the provided SV.
 * UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
 * where x is random hex digit, y is one of 8,9,a,b
 * 
 * Byte layout:
 * - Bytes 0-15: 16 random bytes with specific bits set:
 *   - Byte 6: high nibble = 0x4 (version 4)
 *   - Byte 8: high 2 bits = 0b10 (variant RFC4122)
 */
SV* cmk_generate_uuid_v4(SV *buf_sv) {
   U8 *byte_pos, *hex_pos, *next_dash;
   U8 *buf= (U8*) cmk_prepare_sv_buffer(buf_sv, 36);
   /* Generate 16 random bytes */
   if (RAND_bytes(buf, 16) != 1)
      croak("RAND_bytes failed");
   /* Set version to 4 (bits 12-15 of time_hi_and_version field = byte 6, high nibble) */
   buf[6] = (buf[6] & 0x0F) | 0x40;
   /* Set variant to RFC4122 (bits 6-7 of clock_seq_hi_and_reserved = byte 8, high 2 bits = 0b10) */
   buf[8] = (buf[8] & 0x3F) | 0x80;
   /* convert the 16 bytes to hex notation with dashes (36 chars long) */
   for (hex_pos= buf + 35, byte_pos= buf + 15, next_dash= buf + 10; byte_pos >= buf; byte_pos--) {
      *hex_pos-- = "0123456789ABCDEF"[*byte_pos >> 4];
      *hex_pos-- = "0123456789ABCDEF"[*byte_pos & 0xF];
      if (byte_pos == next_dash && next_dash != buf + 2) {
         *hex_pos-- = '-';
         next_dash -= 2;
      }
   }
   return buf_sv;
}

/* Utility function that dies with one or two strings and the current libssl error message.
 * The 'context' parameter may be NULL to use just one prefix string.
 */
static void
cmk_croak_with_ssl_error(const char *context, const char *err) {
   unsigned long ssl_err = ERR_get_error();
   char ssl_err_str[256] = {0};
   if (ssl_err) {
      ERR_error_string_n(ssl_err, ssl_err_str, sizeof ssl_err_str);
      if (context)
         croak("%s %s: %s", context, err, ssl_err_str);
      else
         croak("%s: %s", err, ssl_err_str);
   } else {
      if (context)
         croak("%s %s", context, err);
      else
         croak("%s", err);
   }
}

/* Decode Base64 text (skipping whitespace) and return raw bytes in a secret_buffer.
 * Croaks on failure.
 * libSSL has routines for this but the documentation mentions a history of bugs
 * which I'd rather not deal with if users compile against old versions.
 */
static secret_buffer *
cmk_decode_base64(const U8 *text, STRLEN text_len) {
   /* might be over-allocated, but that's ok */
   secret_buffer *out= secret_buffer_new((text_len + 1) * 3 / 4, NULL);
   secret_buffer_parse b64;
   secret_buffer_parse_rw dest;

   memset(&b64, 0, sizeof(b64));
   b64.pos= (U8*) text;
   b64.lim= (U8*) text + text_len;
   b64.encoding= SECRET_BUFFER_ENCODING_BASE64;
   secret_buffer_parse_init((secret_buffer_parse*)&dest, out, 0, out->capacity, 0);
   if (!secret_buffer_transcode(&b64, &dest))
      croak("Malformed base64");
   out->len= dest.pos - (U8*) out->data;
   return out;
}

/* Return true if the requested key type is one of the ML-KEM names.
 */
static bool
cmk_is_mlkem_keytype_name(const char *type) {
   int type_len= strlen(type);
   return (type_len == 10 && foldEQ(type, "ML-KEM-512", 10))
       || (type_len == 10 && foldEQ(type, "ML-KEM-768", 10))
       || (type_len == 11 && foldEQ(type, "ML-KEM-1024", 11));
}

/* Return canonical OpenSSL name for ML-KEM type strings, or NULL if not ML-KEM.
 */
static const char *
cmk_mlkem_canonical_name(const char *type) {
   int type_len= strlen(type);
   if (type_len == 10 && foldEQ(type, "ML-KEM-512", 10))
      return "ML-KEM-512";
   if (type_len == 10 && foldEQ(type, "ML-KEM-768", 10))
      return "ML-KEM-768";
   if (type_len == 11 && foldEQ(type, "ML-KEM-1024", 11))
      return "ML-KEM-1024";
   return NULL;
}

/* Return whether cmk_pkey pointer has a public key.
 * Currently, cmk_pkey is a typedef for EVP_PKEY*, and a non-null EVP_PKEY implies it has at
 * least a public key, so just check that it points to something.
 */
bool
cmk_pkey_has_public(cmk_pkey *pkey) {
   /* any non-null EVP_PKEY will have a public half */
   return *pkey != NULL;
}

/* Return whether cmk_pkey has the private half loaded.
 */
bool
cmk_pkey_has_private(cmk_pkey *pkey) {
   if (!*pkey) return false;
   /* From OpenSSL 3.0 onward, i2d_PrivateKey reliably returns false if the private key
      is not loaded.  Before that, for some key types i2d_PrivateKey would fall back to
      exporting the public key, making it useless for this purpose. */
   #if OPENSSL_VERSION_NUMBER < 0x30000000L
   switch (EVP_PKEY_base_id(*pkey)) {
   case EVP_PKEY_RSA2:
   case EVP_PKEY_RSA: {
      /* If RSA 'd' parameter is set, it's a private key */
      const BIGNUM *d= NULL;
      const RSA *rsa = EVP_PKEY_get0_RSA(*pkey);
      if (rsa)
         RSA_get0_key(rsa, NULL, NULL, &d);
      return d != NULL;
   }
   case EVP_PKEY_EC: {
      const EC_KEY *ec = EVP_PKEY_get0_EC_KEY(*pkey);
      return ec != NULL && EC_KEY_get0_private_key(ec) != NULL;
   }
   #if defined(EVP_PKEY_ED25519) || defined(EVP_PKEY_X25519) || defined(EVP_PKEY_ED448) || defined(EVP_PKEY_X448)
   # ifdef EVP_PKEY_ED448
   case EVP_PKEY_ED448:
   # endif
   # ifdef EVP_PKEY_X448
   case EVP_PKEY_X448:
   # endif
   # ifdef EVP_PKEY_ED25519
   case EVP_PKEY_ED25519:
   # endif
   # ifdef EVP_PKEY_X25519
   case EVP_PKEY_X25519:
   # endif
   {
      U8 buf[256]; /* big enough for X448/Ed448 (57 bytes) and some room for the future */
      size_t len = sizeof(buf);
      int ret = EVP_PKEY_get_raw_private_key(*pkey, buf, &len);
      OPENSSL_cleanse(buf, len > 0 && len <= sizeof(buf)? len : sizeof(buf));
      return ret == 1 && len > 0;
   }
   #endif
   default: (void)0;
   }
   /* Unknown type: fallback to i2d_PrivateKey probe */
   #endif
   return i2d_PrivateKey(*pkey, NULL) > 0;
}

/* Clone a cmk_pkey.
 * Currently this just means cloning the EVP_PKEY and replacing the pointer in `dest`.
 * Oddly there doesn't seem to be an API for it, so just serialize and deserialize.
 */
void
cmk_pkey_dup(cmk_pkey *dest, cmk_pkey *src) {
   const char *err = NULL;
   EVP_PKEY *clone = NULL;
   int len;
   U8 *buf = NULL;

   ERR_clear_error();

   /* First, see if there is a private key encoding */
   if (cmk_pkey_has_private(dest)) {
      len = i2d_PrivateKey(*src, NULL);
      /* Full private+public key available: clone via private key only */
      if (!(buf = (U8*) safemalloc((Size_t)len)))
         GOTO_CLEANUP_CROAK("malloc failed");

      unsigned char *p = buf;
      if (i2d_PrivateKey(*src, &p) != len)
         GOTO_CLEANUP_CROAK("i2d_PrivateKey failed");

      const unsigned char *cp = buf;
      if (!d2i_PrivateKey(EVP_PKEY_base_id(*src), &clone, &cp, (long)len))
         GOTO_CLEANUP_CROAK("d2i_PrivateKey failed");
   } else {
      /* No private key (or encoding failed): fall back to public-only */
      len = i2d_PUBKEY(*src, NULL);
      if (len <= 0)
         GOTO_CLEANUP_CROAK("i2d_PUBKEY failed");

      if (!(buf = (U8*) safemalloc((Size_t)len)))
         GOTO_CLEANUP_CROAK("malloc failed");

      unsigned char *p = buf;
      if (i2d_PUBKEY(*src, &p) != len)
         GOTO_CLEANUP_CROAK("i2d_PUBKEY failed");

      const unsigned char *cp = buf;
      if (!d2i_PUBKEY(&clone, &cp, (long)len))
         GOTO_CLEANUP_CROAK("d2i_PUBKEY failed");
   }
cleanup:
   if (buf) {
      /* buf may contain private key DER */
      OPENSSL_cleanse(buf, len);
      Safefree(buf);
   }

   if (err) {
      if (clone)
         EVP_PKEY_free(clone);
      cmk_croak_with_ssl_error("EVP_PKEY_dup", err);
   }

   if (*dest) EVP_PKEY_free(*dest);
   *dest= clone;
}

/* Store a name of the algorithm (and it's major parameter, if relevant) into 'out'.
 * This attempts to be round-trip compatible with cmk_pkey_keygen_params.
 */
void cmk_pkey_get_algorithm_name(cmk_pkey *pk, SV *out) {
   if (!pk || !*pk) {
      sv_setsv(out, &PL_sv_undef);
   }
   else {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
      const char *alg = EVP_PKEY_get0_type_name(*pk);
      if (!alg)
         alg = "UNKNOWN";

      if (EVP_PKEY_is_a(*pk, "RSA") || EVP_PKEY_is_a(*pk, "DSA") || EVP_PKEY_is_a(*pk, "DH")) {
         sv_setpvf(out, "%s:bits=%d", alg, EVP_PKEY_get_bits(*pk));
      }
      else if (EVP_PKEY_is_a(*pk, "EC")) {
         char curve[80];
         size_t len = sizeof(curve);
         if (EVP_PKEY_get_utf8_string_param(
               *pk, OSSL_PKEY_PARAM_GROUP_NAME,
               curve, sizeof(curve), &len))
         {
            sv_setpvf(out, "%s:curve=%s", alg, curve);
         } else {
            sv_setpv(out, alg);
         }
      }
#else /* OpenSSL 1.1.1 */
      int type = EVP_PKEY_base_id(*pk);
      const char *alg = OBJ_nid2sn(type);
      if (!alg)
         alg = "UNKNOWN";

      if (type == EVP_PKEY_RSA || type == EVP_PKEY_DSA || type == EVP_PKEY_DH) {
         sv_setpvf(out, "%s:bits=%d",
            type == EVP_PKEY_RSA? "RSA" : type == EVP_PKEY_DSA? "DSA" : "DH",
            EVP_PKEY_bits(*pk));
      }
      else if (type == EVP_PKEY_EC) {
         EC_KEY *ec = EVP_PKEY_get0_EC_KEY(*pk);
         const EC_GROUP *grp = ec? EC_KEY_get0_group(ec) : NULL;
         int nid = grp? EC_GROUP_get_curve_name(grp) : NID_undef;
         if (nid != NID_undef) {
            sv_setpvf(out, "EC:curve=%s", OBJ_nid2sn(nid));
         } else {
            sv_setpv(out, alg);
         }
      }
#endif
      else { /* "ED25519", "ED448", "X25519", "X448" */
         sv_setpv(out, alg);
         #if OPENSSL_VERSION_NUMBER < 0x30000000L
         {
            STRLEN len;
            char *str= SvPV(out, len);
            for (STRLEN i = 0; i < len; i++)
               str[i] = toUPPER(str[i]);
            SvSETMAGIC(out);
         }
         #endif
      }
   }
}

/* Feature test: EVP RSA keygen controls exist in OpenSSL >= 1.0.2,
 * but LibreSSL does not provide them (even if it defines OPENSSL_VERSION_NUMBER).
 */
#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x10002000L)
#  define CMK_HAVE_EVP_RSA_KEYGEN_CTRLS 1
#else
#  define CMK_HAVE_EVP_RSA_KEYGEN_CTRLS 0
#endif
/* With OpenSSL 3.0, lots of methods that previously didn't use the set0/set1
 * convention got renamed to start using it.
 *  - get0 means it returns an existing pointer.
 *  - get1 means it makes a copy that the caller must free.
 *  - set0 means it transfers ownership and the caller should not free the reference
 *  - set1 means it makes a copy and the caller still owns a reference.
 */
#if OPENSSL_VERSION_NUMBER < 0x30000000L
#  define EVP_PKEY_CTX_set1_rsa_keygen_pubexp EVP_PKEY_CTX_set_rsa_keygen_pubexp
#endif

/* Public/Private Key generation based on string parameters.
 * I'm attempting to use the same names as OpenSSL3 but implement it in 1.1 compatible code.
 */
void
cmk_pkey_keygen_params(cmk_pkey *pk, const char *type, const char **params, int param_count) {
   const char *err= NULL;
   EVP_PKEY_CTX *ctx= NULL;
   EVP_PKEY *pkey= NULL;
   BIGNUM *bignum= NULL;
   secret_buffer *seed= NULL;
#if !CMK_HAVE_EVP_RSA_KEYGEN_CTRLS
   RSA *rsa= NULL;
#endif
   int type_len= strlen(type);

   ERR_clear_error();

   if (type_len == 6 && foldEQ(type, "x25519", 6)) {
      if (param_count > 0)
         GOTO_CLEANUP_CROAK("x25519 does not take any parameters");
      ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
      if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("keygen init (x25519) failed");
   }  
   else if (type_len == 7 && foldEQ(type, "ED25519", 7)) {
      if (param_count > 0)
         GOTO_CLEANUP_CROAK("ed25519 does not take any parameters");
      ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
      if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("keygen init (ED25519) failed");
   }
   else if (type_len == 3 && foldEQ(type, "RSA", 3)) {
      int i, bits= 4096;
      unsigned long pubexp_ul = RSA_F4; /* default 65537 */
      int have_pubexp = 0;

      for (i= 0; i < param_count; i++) {
         const char *p = params[i];
         const char *eq = strchr(p, '=');
         int len= eq? eq - p : strlen(p);
         const char *value= eq? eq + 1 : "";
         if (len == 6 && memcmp(p, "pubexp", 6) == 0
            || len == 17 && memcmp(p, "rsa_keygen_pubexp", 17) == 0
         ) {
            char *end;
            long exp= strtol(value, &end, 10);
            if (*end != '\0' || end == value || exp < 3)
               GOTO_CLEANUP_CROAK("invalid rsa_keygen_pubexp");
            pubexp_ul= exp;
            have_pubexp= 1;
         }
         else if (len == 4 && memcmp(p, "bits", 4) == 0
            || len == 15 && memcmp(p, "rsa-keygen-bits", 15) == 0
         ) {
            char *end;
            long v= strtol(value, &end, 10);
            if (*end != '\0' || end == value || v < 512)
               GOTO_CLEANUP_CROAK("invalid 'rsa-keygen-bits' value");
            bits= (int) v;
         }
         else {
            GOTO_CLEANUP_CROAK("Unknown RSA parameter");
         }
      }
      
      #if CMK_HAVE_EVP_RSA_KEYGEN_CTRLS
      ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
      if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("keygen init (RSA) failed");
      if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0)
         GOTO_CLEANUP_CROAK("set_rsa_keygen_bits failed");
      if (have_pubexp) {
         bignum = BN_new();
         if (!bignum || !BN_set_word(bignum, pubexp_ul))
            GOTO_CLEANUP_CROAK("BN_new/BN_set_word failed");
         if (EVP_PKEY_CTX_set1_rsa_keygen_pubexp(ctx, bignum) <= 0)
            GOTO_CLEANUP_CROAK("EVP_PKEY_CTX_set1_rsa_keygen_pubexp failed");
      }
      #else
      /* If this version of OpenSSL/LibreSSL can't modify the exponent in the key
         generating context, we need to create the key the "old way" */
      bignum = BN_new();
      if (!bignum || !BN_set_word(bignum, pubexp_ul))
         GOTO_CLEANUP_CROAK("BN_new/BN_set_word failed");

      rsa = RSA_new();
      if (!rsa)
         GOTO_CLEANUP_CROAK("RSA_new failed");

      if (RSA_generate_key_ex(rsa, bits, bignum, NULL) != 1)
         GOTO_CLEANUP_CROAK("RSA_generate_key_ex failed");

      /* assign rsa into new EVP_PKEY */
      pkey = EVP_PKEY_new();
      if (!pkey)
         GOTO_CLEANUP_CROAK("EVP_PKEY_new failed");

      /* Transfers ownership of rsa to pkey on success. */
      if (EVP_PKEY_assign_RSA(pkey, rsa) != 1)
         GOTO_CLEANUP_CROAK("EVP_PKEY_assign_RSA failed");
      rsa = NULL;

      /* bypass the call to EVP_PKEY_keygen on the context */
      goto cleanup;
      #endif
   }
   else if (type_len == 2 && foldEQ(type, "EC", 2)) {
      int i;
      ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
      if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("keygen init (EC) failed");
      for (i= 0; i < param_count; i++) {
         const char *p = params[i];
         const char *eq = strchr(p, '=');
         int len= eq? eq - p : strlen(p);
         const char *value= eq? eq + 1 : "";
         if (len == 5 && memcmp(p, "group", 5) == 0
            || len == 5 && memcmp(p, "curve", 5) == 0
         ) {
            int curve_nid = OBJ_txt2nid(value);
            if (curve_nid == NID_undef)
               GOTO_CLEANUP_CROAK("unknown EC group");
            if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, curve_nid) <= 0)
               GOTO_CLEANUP_CROAK("set_ec_paramgen_curve_nid failed");

            if (EVP_PKEY_CTX_set_ec_param_enc(ctx, OPENSSL_EC_NAMED_CURVE) <= 0)
               GOTO_CLEANUP_CROAK("set_ec_param_enc failed");
         }
         else {
            GOTO_CLEANUP_CROAK("Unknown EC parameter");
         }
      }
   }
#if defined(HAVE_MLKEM_KEYGEN)
   else if (cmk_is_mlkem_keytype_name(type)) {
      int i;
      const char *mlkem_type= cmk_mlkem_canonical_name(type);
      OSSL_PARAM keygen_params[2];
      keygen_params[0]= OSSL_PARAM_construct_end();
      keygen_params[1]= OSSL_PARAM_construct_end();
      if (!mlkem_type)
         GOTO_CLEANUP_CROAK("Unknown ML-KEM type");

      ctx = EVP_PKEY_CTX_new_from_name(NULL, mlkem_type, NULL);
      if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("keygen init (ML-KEM) failed");

      /* Parse optional ML-KEM keygen params from Perl strings. */
      for (i= 0; i < param_count; i++) {
         const char *p = params[i];
         const char *eq = strchr(p, '=');
         int len= eq? eq - p : strlen(p);
         const char *value= eq? eq + 1 : "";

         if (len == 11 && memcmp(p, "seed_base64", 11) == 0) {
            if (seed)
               GOTO_CLEANUP_CROAK("Duplicate ML-KEM parameter 'seed_base64'");
            seed= cmk_decode_base64((const U8*) value, strlen(value));
            keygen_params[0]= OSSL_PARAM_construct_octet_string(
               OSSL_PKEY_PARAM_ML_KEM_SEED, seed->data, seed->len);
            keygen_params[1]= OSSL_PARAM_construct_end();
         }
         else
            GOTO_CLEANUP_CROAK("Unknown ML-KEM parameter");
      }

      if (keygen_params[0].key && EVP_PKEY_CTX_set_params(ctx, keygen_params) <= 0)
         GOTO_CLEANUP_CROAK("set_params (ML-KEM) failed");
   }
#endif
   else {
      if (cmk_is_mlkem_keytype_name(type))
         croak("key type '%s' requires OpenSSL 3.5 or newer", type);
      croak("unknown key type '%s'", type);
   }
   if (EVP_PKEY_keygen(ctx, &pkey) <= 0)
      GOTO_CLEANUP_CROAK("keygen failed");
cleanup:
   if (ctx)
      EVP_PKEY_CTX_free(ctx);
#if !CMK_HAVE_EVP_RSA_KEYGEN_CTRLS
   if (rsa)
      RSA_free(rsa);
#endif
   if (bignum)
      BN_free(bignum);
   if (err)
      cmk_croak_with_ssl_error("keygen:", err);
   if (*pk) EVP_PKEY_free(*pk);
   *pk= pkey;
}

/* Like cmk_pkey_keygen_params, but parse params out of ":param=value" suffix of the type.
 */
void
cmk_pkey_keygen(cmk_pkey *pk, const char *type_and_params) {
   const char *type, *param_start= strchr(type_and_params, ':');
   int param_count= 0;
   char **params= NULL;
   if (param_start) {
      STRLEN len= strlen(type_and_params);
      char *parambuf;
      /* count params */
      { const char *ch= param_start;
         do {
            if (ch[1] != ':' && ch[1] != '\0')
               ++param_count;
         } while ((ch= strchr(ch+1, ':')));
      }
      /* create a writable buffer so we can replace ':' with NUL */
      Newx(parambuf, len+1, char);
      SAVEFREEPV(parambuf);
      memcpy(parambuf, type_and_params, len+1);
      Newx(params, param_count, char*);
      SAVEFREEPV(params);
      param_count= 0;
      {
         char *ch= strchr(parambuf, ':');;
         do {
            *ch= '\0';
            if (ch[1] != ':' && ch[1] != '\0')
               params[param_count++]= ch+1;
         } while ((ch= strchr(ch+1, ':')));
      }
      type= parambuf;
   } else
      type= type_and_params;
   cmk_pkey_keygen_params(pk, type, (const char **) params, param_count);
}

/* MAGIC for storing an OpenSSL EVP_PKEY object on an arbitrary SV */

#ifdef USE_ITHREADS
static int cmk_pkey_magic_dup(pTHX_ MAGIC *mg, CLONE_PARAMS *param) {
   if (mg->mg_ptr) {
      cmk_pkey pkey= (EVP_PKEY*) mg->mg_ptr;
      cmk_pkey_dup((cmk_pkey*) &mg->mg_ptr, &pkey);
   }
   PERL_UNUSED_VAR(param);
   return 0;
};
#define SET_MGf_DUP_FLAG(mg) do { magic->mg_flags |= MGf_DUP; } while (0)
#else
#define cmk_pkey_magic_dup 0
#define SET_MGf_DUP_FLAG(mg) ((void)0)
#endif

static int cmk_pkey_magic_free(pTHX_ SV *sv, MAGIC *mg) {
   if (mg->mg_ptr) EVP_PKEY_free((EVP_PKEY*) mg->mg_ptr);
   return 0;
}

static MGVTBL cmk_pkey_magic_vtbl = {
   NULL, NULL, NULL, NULL,
   cmk_pkey_magic_free,
   NULL,
   cmk_pkey_magic_dup
#ifdef MGf_LOCAL
   ,NULL
#endif
};

/* Crypt::MultiKey::PKey stores a EVP_KEY in Magic.  OpenSSL routines can rewrite the pointer,
 * so instead of load/save API I just return a pointer-to-pointer that can be written.
 * If EVP_PKEY magic doesn't exist, it gets created with a pointer that is initially NULL.
 */
cmk_pkey*
cmk_pkey_from_magic(SV *objref, int flags) {
   dTHX;
   MAGIC *magic= cmk_get_X_magic(aTHX_ objref, flags, &cmk_pkey_magic_vtbl, "cmk_pkey");
   /*
   warn("cmk_pkey_from_magic sv=%s flags=%s%s%s magic=%p magic->mg_ptr=%p\n",
      (!objref? "NULL" : SvROK(objref)? "REF" : "undef"),
      (flags & CMK_MAGIC_AUTOCREATE)? "AUTOCREATE ":" ",
      (flags & CMK_MAGIC_OR_DIE)? "OR_DIE ":" ",
      (flags & CMK_MAGIC_UNDEF_OK)? "UNDEF_OK":"",
      magic, (magic? magic->mg_ptr : NULL));
   */
   /* currently the cmk_pkey is an alias for EVP_PKEY*, so the address of magic pointer itself
    * becomes the return value */
   return magic? (cmk_pkey*) &magic->mg_ptr : NULL;
}

/* Handy accessors for public and private key which check the type of key actually
 * present in MAGIC and croak with useful error messages. */

cmk_pkey*
cmk_get_pubkey(SV *objref) {
   cmk_pkey *pk= cmk_pkey_from_magic(objref, 0);
   if (!pk || !*pk)
      croak("No public/private key loaded");
   return pk;
}

cmk_pkey*
cmk_get_privkey(SV *objref) {
   cmk_pkey *pk= cmk_pkey_from_magic(objref, 0);
   if (!pk || !*pk)
      croak("No public/private key loaded");
   if (!cmk_pkey_has_private(pk))
      croak("Private half of key is not loaded/decrypted");
   return pk;
}

/* Import from RFC 5280 SubjectPublicKeyInfo format */
void
cmk_pkey_import_spki(cmk_pkey *pk, const U8 *buf, STRLEN buf_len) {
   EVP_PKEY *key= NULL;

   ERR_clear_error();

   if (!d2i_PUBKEY(&key, &buf, buf_len) || !key)
      cmk_croak_with_ssl_error("import_pubkey", "Decoding SubjectPublicKeyInfo failed");
   if (*pk) EVP_PKEY_free(*pk);
   *pk= key;
}

void
cmk_pkey_export_spki(cmk_pkey *pk, SV *buf_out) {
   secret_buffer *sb;
   U8 *buf;

   ERR_clear_error();

   /* encode the public key into a scalar */
   int serialized_len= i2d_PUBKEY(*pk, NULL);
   if (serialized_len <= 0)
      cmk_croak_with_ssl_error("export_spki", "i2d_PUBKEY failed");
   /* buffer maight be a secret_buffer reference */
   if ((sb= secret_buffer_from_magic(buf_out, 0))) {
      secret_buffer_set_len(sb, serialized_len);
      buf= (U8*) sb->data;
   }
   else {
      buf= (U8*) cmk_prepare_sv_buffer(buf_out, serialized_len);
   }
   if (i2d_PUBKEY(*pk, &buf) != serialized_len)
      cmk_croak_with_ssl_error("export_spki", "i2d_PUBKEY");
}

/* Load the private key from the buffer and store it into MAGIC on the ::PKey object.
 * The buffer should contain ASN.1 DER bytes of PKCS#8 which may be storing an encrypted private
 * key that requires a password to decrypt.  PKCS#8 also stores the PDK iterations and other
 * encryption parameters, so only the original password is required.
 * Returns false on incorrect password, and dies on any other type of error.
 */
bool
cmk_pkey_import_pkcs8(cmk_pkey *pk, const U8 *buf, STRLEN buf_len, const char *pw, STRLEN pw_len) {
   const char *err= NULL;
   PKCS8_PRIV_KEY_INFO *p8inf= NULL;
   EVP_PKEY *pkey= NULL;
   const U8 *buf_copy = buf;
   X509_SIG *p8= NULL;

   ERR_clear_error();

   /* Try to decode as encrypted PKCS8 (X509_SIG) first */
   p8 = d2i_X509_SIG(NULL, &buf_copy, buf_len);
   if (p8) {
      /* Successfully decoded as X509_SIG - this means it's encrypted */
      if (!pw_len)
         goto cleanup; /* requires password */

      /* Decrypt to get PKCS8_PRIV_KEY_INFO */
      p8inf = PKCS8_decrypt(p8, pw, pw_len);
      if (!p8inf)
         goto cleanup; /* wrong password */
   }
   else {
      /* Failed to decode as X509_SIG, try as unencrypted PKCS8_PRIV_KEY_INFO */
      buf_copy = buf;
      p8inf = d2i_PKCS8_PRIV_KEY_INFO(NULL, &buf_copy, buf_len);

      if (!p8inf)
         GOTO_CLEANUP_CROAK("Failed to decode PKCS8 structure (not valid PKCS8 format)");

      if (pw_len)
         /* Warn that password was provided but key is not encrypted */
         warn("Password provided but PKCS8 private key is not encrypted");
   }

   /* Convert PKCS8_PRIV_KEY_INFO to EVP_PKEY */
   pkey = EVP_PKCS82PKEY(p8inf);
   if (!pkey)
      GOTO_CLEANUP_CROAK("Failed to convert PKCS8 to EVP_PKEY");

cleanup:
   if (p8inf)
      PKCS8_PRIV_KEY_INFO_free(p8inf);
   if (p8)
      X509_SIG_free(p8);
   if (err)
      cmk_croak_with_ssl_error("import_pkcs8", err);
   if (pkey) {
      if (*pk) EVP_PKEY_free(*pk);
      *pk= pkey;
      return true;
   } else
      return false;
}

/* Save the private key from ::PKey MAGIC into the supplied buffer.
 * The pasword parameter is optional, and if supplied, results in an encrypted private key.
 */
void
cmk_pkey_export_pkcs8(cmk_pkey *pk, const char *pw, STRLEN pw_len, int kdf_iters, SV *buf_out) {
   const char *err= NULL;
   PKCS8_PRIV_KEY_INFO *p8inf= NULL;
   X509_SIG *p8= NULL;
   U8 *buf;
   secret_buffer *sb;
   int serialized_len;

   ERR_clear_error();

   /* Convert EVP_PKEY to PKCS8_PRIV_KEY_INFO */
   p8inf= EVP_PKEY2PKCS8(*pk);
   if (!p8inf)
      GOTO_CLEANUP_CROAK("Failed to convert key to PKCS8 format");

   /* Encrypt with specified iteration count */
   if (pw_len) {
      /* Validate iteration count for encrypted keys */
      if (kdf_iters < 1)
         GOTO_CLEANUP_CROAK("KDF iteration count must be positive");

      p8= PKCS8_encrypt(
         -1,                          /* use default PKCS#12 PBE algorithm */
         EVP_aes_256_cbc(),           /* cipher */
         (const char *)pw, pw_len,
         NULL, 0,                     /* salt (NULL = auto-generate) */
         kdf_iters,                   /* iteration count */
         p8inf
      );
      if (!p8)
         GOTO_CLEANUP_CROAK("PKCS8 encryption failed");

      /* Serialize to DER format */
      serialized_len= i2d_X509_SIG(p8, NULL);
   }
   else {
      serialized_len = i2d_PKCS8_PRIV_KEY_INFO(p8inf, NULL);
   }

   if (serialized_len <= 0)
      GOTO_CLEANUP_CROAK("Can't determine PKCS8 serialized length");
   if ((sb= secret_buffer_from_magic(buf_out, 0))) {
      secret_buffer_set_len(sb, serialized_len);
      buf= (U8*) sb->data;
   } else {
      buf= (U8*) cmk_prepare_sv_buffer(buf_out, serialized_len);
   }
   
   if (pw_len) {
      if (i2d_X509_SIG(p8, &buf) != serialized_len)
         GOTO_CLEANUP_CROAK("Can't serialize PKCS8");
   }
   else {
      if (i2d_PKCS8_PRIV_KEY_INFO(p8inf, &buf) != serialized_len)
         GOTO_CLEANUP_CROAK("Can't serialize PKCS8");
   }
cleanup:
   if (p8)
      X509_SIG_free(p8);
   if (p8inf)
      PKCS8_PRIV_KEY_INFO_free(p8inf);
   if (err)
      cmk_croak_with_ssl_error("export_pkcs8", err);
}

/* This function generates a shared secret from a public key which only the private key can
 * recreate.  The secret is appended to skey_buf, and the parameters for recreating it are
 * stored in the 'tumbler' hash.
 *   - For RSA keys, this generates a random wrapper key, and encrypts the wrapper key using RSA.
 *   - For DSA-like key types, this creates an ephemeral key, derives the shared secret, and
 *     stores the public half of the ephemeral key in the tumbler.
 */
void
cmk_pkey_generate_key_material(cmk_pkey *pubkey, HV *tumbler, secret_buffer *skey_buf) {
   const char *err = NULL;
   EVP_PKEY *ephemeral = NULL;
   EVP_PKEY_CTX *pctx = NULL;
   EVP_PKEY_CTX *ctx = NULL;
   SV *sv= NULL;
   int type= EVP_PKEY_base_id(*pubkey);

   ERR_clear_error();

   /* RSA keys encrypt/decrypt directly, but DSA-style keys need to create an ephermeral
    * key to perform a handshake with, to produce a shared secret.
    */
   if (type == EVP_PKEY_EC || type == EVP_PKEY_DH
      #ifdef EVP_PKEY_X25519
      || type == EVP_PKEY_X25519
      #endif
      #ifdef EVP_PKEY_X448
      || type == EVP_PKEY_X448
      #endif
   ) {
      U8 *ephemeral_pub;    /* DER ephemeral pub for X25519 */
      int ephemeral_pub_len;
      size_t shared_len;

      /* Generate ephemeral keypair of same type */
      pctx = EVP_PKEY_CTX_new(*pubkey, NULL);
      if (!pctx
         || EVP_PKEY_keygen_init(pctx) <= 0
         || EVP_PKEY_keygen(pctx, &ephemeral) <= 0
         || !ephemeral
      )
         GOTO_CLEANUP_CROAK("Ephemeral key generation failed");

      /* Derive shared secret: ephemeral (private) + MultiKey::PKey object (public) */
      ctx= EVP_PKEY_CTX_new(ephemeral, NULL);
      if (!ctx)
         GOTO_CLEANUP_CROAK("EVP_PKEY_CTX_new failed");
      if (EVP_PKEY_derive_init(ctx) <= 0)
         GOTO_CLEANUP_CROAK("Derive init failed");
      if (EVP_PKEY_derive_set_peer(ctx, *pubkey) <= 0)
         GOTO_CLEANUP_CROAK("Derive set-peer failed");

      /* Query required length and enlarge the destination buffer */
      if (EVP_PKEY_derive(ctx, NULL, &shared_len) <= 0 || shared_len == 0)
         GOTO_CLEANUP_CROAK("Derive (size) failed");
      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + shared_len);

      /* Derive the secret and update the buffer's length to match */
      if (EVP_PKEY_derive(ctx, (U8*) skey_buf->data + skey_buf->len, &shared_len) <= 0)
         GOTO_CLEANUP_CROAK("Deriving shared secret failed");
      skey_buf->len += shared_len;

      /* DER-encode ephemeral public key (SPKI) for storage */
      if ((ephemeral_pub_len = i2d_PUBKEY(ephemeral, NULL)) <= 0)
         GOTO_CLEANUP_CROAK("i2d_PUBKEY (size) failed");
      ephemeral_pub= (U8*) cmk_prepare_sv_buffer((sv= newSVpvs("")), ephemeral_pub_len);
      if (i2d_PUBKEY(ephemeral, &ephemeral_pub) != ephemeral_pub_len)
         GOTO_CLEANUP_CROAK("i2d_PUBKEY failed");

      /* Store ephemeral pubkey */
      if (!hv_stores(tumbler, "ephemeral_pubkey", sv))
         GOTO_CLEANUP_CROAK("hv_store failed");
      sv= NULL; /* HV takes ownership */
   }
   else if (type == EVP_PKEY_RSA || type == EVP_PKEY_RSA2
      #ifdef EVP_PKEY_RSA_PSS
      || type == EVP_PKEY_RSA_PSS
      #endif
   ) {
      size_t rsa_ct_len= 0;
      U8 *buf_pos;
      /* ---- RSA branch (RSA-OAEP) ---- */

      /* Generate random wrap key */
      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + CMK_RSA_KEYMATERIAL_LEN);
      buf_pos= (U8*) skey_buf->data + skey_buf->len;
      if (RAND_bytes(buf_pos, CMK_RSA_KEYMATERIAL_LEN) != 1)
         GOTO_CLEANUP_CROAK("RAND_bytes for wrap key failed");
      skey_buf->len += CMK_RSA_KEYMATERIAL_LEN;

      /* 2. RSA-OAEP encrypt wrap key with public_key */
      ctx = EVP_PKEY_CTX_new(*pubkey, NULL);
      if (!ctx
         || EVP_PKEY_encrypt_init(ctx) <= 0
         || EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0
         || EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0
         || EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0)
         GOTO_CLEANUP_CROAK("RSA encrypt init failed");

      if (EVP_PKEY_encrypt(ctx, NULL, &rsa_ct_len, buf_pos, CMK_RSA_KEYMATERIAL_LEN) <= 0
         || rsa_ct_len == 0)
         GOTO_CLEANUP_CROAK("RSA encrypt size query failed");

      if (EVP_PKEY_encrypt(ctx,
            (U8*) cmk_prepare_sv_buffer((sv= newSVpvs("")), rsa_ct_len), &rsa_ct_len,
            buf_pos, CMK_RSA_KEYMATERIAL_LEN) <= 0)
         GOTO_CLEANUP_CROAK("RSA encrypt failed");

      /* Store encrypted aes_key */
      if (!hv_stores(tumbler, "rsa_key_ciphertext", sv))
         GOTO_CLEANUP_CROAK("hv_store failed");
      sv= NULL; /* HV takes ownership */
   }
#if defined(HAVE_EVP_PKEY_ENCAPSULATE)
   else if ((ctx = EVP_PKEY_CTX_new(*pubkey, NULL))
      && EVP_PKEY_encapsulate_init(ctx, NULL) > 0
   ) {
      size_t kem_ct_len= 0;
      size_t kem_shared_len= 0;
      U8 *kem_shared;
      U8 *kem_ciphertext;

      if (EVP_PKEY_encapsulate(ctx, NULL, &kem_ct_len, NULL, &kem_shared_len) <= 0
         || kem_ct_len == 0 || kem_shared_len == 0)
         GOTO_CLEANUP_CROAK("KEM encapsulate size query failed");

      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + kem_shared_len);
      kem_shared= (U8*) skey_buf->data + skey_buf->len;
      kem_ciphertext= (U8*) cmk_prepare_sv_buffer((sv= newSVpvs("")), kem_ct_len);

      if (EVP_PKEY_encapsulate(ctx, kem_ciphertext, &kem_ct_len, kem_shared, &kem_shared_len) <= 0)
         GOTO_CLEANUP_CROAK("KEM encapsulate failed");

      SvCUR_set(sv, kem_ct_len);
      skey_buf->len += kem_shared_len;
      if (!hv_stores(tumbler, "kem_ciphertext", sv))
         GOTO_CLEANUP_CROAK("hv_store failed");
      sv= NULL;
   }
#endif
   else {
      ERR_clear_error();
      #ifdef EVP_PKEY_ED25519
      if (type == EVP_PKEY_ED25519
         #ifdef EVP_PKEY_ED448
         || type == EVP_PKEY_ED448
         #endif
      ) GOTO_CLEANUP_CROAK("ED25519 and ED448 keys can only be used for signatures");
      #endif
      GOTO_CLEANUP_CROAK("Unsupported key type");
   }

cleanup:
   if (ctx) EVP_PKEY_CTX_free(ctx);
   if (pctx) EVP_PKEY_CTX_free(pctx);
   if (ephemeral) EVP_PKEY_free(ephemeral);
   if (sv) /* if hv_stores fails, the thing we tried to store needs freed */
      SvREFCNT_dec(sv);
   if (err) cmk_croak_with_ssl_error("generate_key_material", err);
}

/* This function re-creates the shared-secret from the "tumbler" of an earlier call to
 * cmk_pkey_generate_key_material, and appends the secret to skey_buf.
 * - For RSA keys: decrypting rsa_key_ciphertext
 * - For DSA-like keys: deriving the shared secret from the ephemeral_pubkey
 */
void
cmk_pkey_recreate_key_material(cmk_pkey *pk, HV *tumbler, secret_buffer *skey_buf) {
   const char *err = NULL;
   EVP_PKEY *ephemeral_pub = NULL;
   EVP_PKEY_CTX *ctx = NULL;
   SV **svp;
   int type = EVP_PKEY_base_id(*pk);

   /* Croak with a clean error message if the private half of the key isn't loaded.
    * Also this avoids a LibreSSL segfault for X25519 keys if you call derive
    * without the private half loaded. */
   if (!cmk_pkey_has_private(pk))
      croak("Private key not loaded");

   ERR_clear_error();

   /* Determine key type and decrypt accordingly */
   if (type == EVP_PKEY_EC || type == EVP_PKEY_DH
      #ifdef EVP_PKEY_X25519
      || type == EVP_PKEY_X25519
      #endif
      #ifdef EVP_PKEY_X448
      || type == EVP_PKEY_X448
      #endif
   ) {
      /* ---- DSA-like branch: derive shared secret from ephemeral public key ---- */
      STRLEN ephemeral_pub_der_len;
      U8 *ephemeral_pub_der;
      size_t shared_len;
      const U8 *p;

      /* Extract ephemeral public key and KDF salt */
      svp = hv_fetchs(tumbler, "ephemeral_pubkey", 0);
      if (!svp || !*svp || !SvOK(*svp))
         GOTO_CLEANUP_CROAK("Missing ephemeral_pubkey");
      ephemeral_pub_der = (U8*)secret_buffer_SvPVbyte(*svp, &ephemeral_pub_der_len);

      /* Decode ephemeral public key from DER */
      p = ephemeral_pub_der;  /* d2i advances the pointer */
      ephemeral_pub = d2i_PUBKEY(NULL, &p, ephemeral_pub_der_len);
      if (!ephemeral_pub)
         GOTO_CLEANUP_CROAK("Failed to decode ephemeral public key");

      /* Derive shared secret: private_key + ephemeral_pub */
      ctx = EVP_PKEY_CTX_new(*pk, NULL);
      if (!ctx ||
         EVP_PKEY_derive_init(ctx) <= 0 ||
         EVP_PKEY_derive_set_peer(ctx, ephemeral_pub) <= 0
      )
         GOTO_CLEANUP_CROAK("Derive init failed");

      /* Query required length and enlarge the destination buffer */
      if (EVP_PKEY_derive(ctx, NULL, &shared_len) <= 0 || shared_len == 0)
         GOTO_CLEANUP_CROAK("Derive (size) failed");
      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + shared_len);

      /* Derive the secret and update the buffer's length to match */
      if (EVP_PKEY_derive(ctx, (U8*) skey_buf->data + skey_buf->len, &shared_len) <= 0)
         GOTO_CLEANUP_CROAK("Deriving shared secret failed");
      skey_buf->len += shared_len;
   }
   else if (type == EVP_PKEY_RSA || type == EVP_PKEY_RSA2
      #ifdef EVP_PKEY_RSA_PSS
      || type == EVP_PKEY_RSA_PSS
      #endif
   ) {
      /* ---- RSA branch: decrypt wrap key using RSA-OAEP ---- */
      STRLEN rsa_ct_len;
      U8 *rsa_ct;
      size_t decoded_len;

      /* Extract encrypted wrap key */
      svp = hv_fetchs(tumbler, "rsa_key_ciphertext", 0);
      if (!svp || !*svp || !SvOK(*svp))
         GOTO_CLEANUP_CROAK("Missing rsa_key_ciphertext");
      rsa_ct = (U8*)secret_buffer_SvPVbyte(*svp, &rsa_ct_len);

      /* RSA-OAEP decrypt to get wrap key */
      ctx = EVP_PKEY_CTX_new(*pk, NULL);
      if (!ctx
         || EVP_PKEY_decrypt_init(ctx) <= 0
         || EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0
         || EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0
         || EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0)
         GOTO_CLEANUP_CROAK("RSA decrypt init failed");

      /* Oddly, this wants a larger size for the buffer, but on the second call it
       * returns the correct number of bytes decoded...  So need to allocate the SecretBuffer
       * larger and then shrink it.
       */
      if (EVP_PKEY_decrypt(ctx, NULL, &decoded_len, rsa_ct, rsa_ct_len) <= 0)
         GOTO_CLEANUP_CROAK("RSA encrypt size query failed");
      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + decoded_len);

      if (EVP_PKEY_decrypt(ctx, (U8*) skey_buf->data + skey_buf->len, &decoded_len, rsa_ct, rsa_ct_len) <= 0)
         GOTO_CLEANUP_CROAK("RSA decrypt failed");
      skey_buf->len += decoded_len;
   }
#if defined(HAVE_EVP_PKEY_ENCAPSULATE)
   else if ((ctx = EVP_PKEY_CTX_new(*pk, NULL))
      && EVP_PKEY_decapsulate_init(ctx, NULL) > 0
   ) {
      STRLEN kem_ct_len;
      U8 *kem_ct;
      size_t kem_shared_len= 0;

      /* KEM branch: decapsulate ciphertext from tumbler. */
      svp = hv_fetchs(tumbler, "kem_ciphertext", 0);
      if (!svp || !*svp || !SvOK(*svp))
         GOTO_CLEANUP_CROAK("Missing kem_ciphertext");
      kem_ct = (U8*) secret_buffer_SvPVbyte(*svp, &kem_ct_len);

      if (EVP_PKEY_decapsulate(ctx, NULL, &kem_shared_len, kem_ct, kem_ct_len) <= 0
         || kem_shared_len == 0)
         GOTO_CLEANUP_CROAK("KEM decapsulate size query failed");

      secret_buffer_alloc_at_least(skey_buf, skey_buf->len + kem_shared_len);
      if (EVP_PKEY_decapsulate(ctx, (U8*) skey_buf->data + skey_buf->len,
            &kem_shared_len, kem_ct, kem_ct_len) <= 0)
         GOTO_CLEANUP_CROAK("KEM decapsulate failed");
      skey_buf->len += kem_shared_len;
   }
#endif
   else {
      ERR_clear_error();
      #ifdef EVP_PKEY_ED25519
      if (type == EVP_PKEY_ED25519
         #ifdef EVP_PKEY_ED448
         || type == EVP_PKEY_ED448
         #endif
      ) GOTO_CLEANUP_CROAK("ED25519 and ED448 keys can only be used for signatures");
      #endif
      GOTO_CLEANUP_CROAK("Unsupported key type");
   }

cleanup:
   if (ctx) EVP_PKEY_CTX_free(ctx);
   if (ephemeral_pub) EVP_PKEY_free(ephemeral_pub);
   if (err) cmk_croak_with_ssl_error("recreate_key_material", err);
}

/* Simple implementation of SHA256 that hashes a combined list of items,
 * each of which can be a plain scalar, scalar-ref, SecretBuffer, or Span.
 */
void
cmk_sha256(U8 dest[32], SV **input, size_t n_items) {
   const char *err= NULL;
   EVP_MD_CTX *ctx = NULL;
   unsigned int outlen = 0;

   ERR_clear_error();

   ctx = EVP_MD_CTX_new();
   if (!ctx)
      GOTO_CLEANUP_CROAK("EVP_MD_CTX_new failed");

   if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1)
      GOTO_CLEANUP_CROAK("EVP_DigestInit_ex(SHA-256) failed");

   for (size_t i = 0; i < n_items; i++) {
      STRLEN len = 0;
      const U8 *buf = (const U8*) secret_buffer_SvPVbyte(input[i], &len);

      /* Allow empty items: update with 0 bytes is fine */
      if (len > 0) {
         if (EVP_DigestUpdate(ctx, buf, (size_t)len) != 1)
            GOTO_CLEANUP_CROAK("EVP_DigestUpdate failed");
      }
   }

   if (EVP_DigestFinal_ex(ctx, dest, &outlen) != 1)
      GOTO_CLEANUP_CROAK("EVP_DigestFinal_ex failed");

cleanup:
   if (ctx) EVP_MD_CTX_free(ctx);
   if (err) cmk_croak_with_ssl_error("sha256", err);
   if (outlen != 32)
      /* Should never happen for SHA-256 */
      croak("sha256: unexpected digest output length %u", outlen);
}

/* This runs HKDF on the key material to generate an AES key.
 * It reads the cipher from the encryption parameters to know how large to make the key.
 * It also creates a random salt and stores that into the encryption parameters.
 * It uses a default HKDF "info" that can be overridden in the encryption params.
 */
secret_buffer *
cmk_hkdf(HV *params, secret_buffer *key_material) {
   const char *err= NULL;
   secret_buffer *out_buf= NULL;
   EVP_PKEY_CTX *kdf= NULL;
   size_t size, wrote;
   U8 *buf, *info;
   STRLEN len, info_len, salt_len;
   U8 *salt, salt_buf[CMK_KDF_SALT_LEN];
   SV **svp, *sv;

   ERR_clear_error();

   /* find out what key size is needed */
   svp= hv_fetchs(params, "cipher", 0);
   if (svp && *svp && SvOK(*svp)) {
      buf= (U8*) secret_buffer_SvPVbyte(*svp, &len);
      if (len == 11 && memcmp(buf, "AES-256-GCM", 11) == 0) {
         size= 32;
      } else if (len == 11 && memcmp(buf, "AES-256-XTS", 11) == 0) {
         size= 64; /* uses AES 256 x 2 for 512 bits (64 bytes) of key */
      } else {
         croak("Unsupported cipher");
      }
   } else {
      size= 32;
      if (!hv_stores(params, "cipher", (sv=newSVpvs("AES-256-GCM"))))
         GOTO_CLEANUP_CROAK("hv_store failed");
      sv= NULL;
   }

   svp= hv_fetchs(params, "size", 0);
   if (svp && *svp && SvOK(*svp)) {
      if (!looks_like_integer(*svp) || SvIV(*svp) <= 0)
         croak("Expected positive integer for 'size'");
      size= SvIV(*svp);
   }

   out_buf= secret_buffer_new(size, NULL);
   
   /* determine the 'info' parameter */
   svp= hv_fetchs(params, "kdf_info", 0);
   if (svp && *svp) {
      /* info may be explicitly set to an empty string or undef */
      if (!SvOK(*svp)) {
         info = "";
         info_len = 0;
      } else {
         info= (U8*) secret_buffer_SvPVbyte(*svp, &info_len);
         if (info_len == 0)
            info = "";
      }
   } else {
      info= (U8*) "Crypt::MultiKey";
      info_len= 15;
   }

   /* was salt provided? */
   svp = hv_fetchs(params, "kdf_salt", 0);
   if (svp && *svp) {
      /* info may be explicitly set to an empty string or undef */
      if (!SvOK(*svp)) {
         salt = "";
         salt_len = 0;
      } else {
         salt = (U8*) secret_buffer_SvPVbyte(*svp, &salt_len);
         if (salt_len == 0)
            salt = "";
      }
   } else {
      /* kdf_salt key not present at all ==> generate random salt and store it */
      if (RAND_bytes(salt_buf, sizeof salt_buf) != 1)
         GOTO_CLEANUP_CROAK("Failed to generate HKDF salt");
      if (!hv_stores(params, "kdf_salt", (sv = newSVpvn((char*)salt_buf, sizeof salt_buf))))
         GOTO_CLEANUP_CROAK("hv_store failed");
      sv = NULL; /* hv owns it now */
      salt = salt_buf;
      salt_len = sizeof salt_buf;
   }

   wrote= size;
   kdf = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
   if (!kdf
      || EVP_PKEY_derive_init(kdf) <= 0
      || EVP_PKEY_CTX_set_hkdf_md(kdf, EVP_sha256()) <= 0
      || EVP_PKEY_CTX_set1_hkdf_salt(kdf, salt, salt_len) <= 0
      || EVP_PKEY_CTX_add1_hkdf_info(kdf, info, info_len) <= 0
      || EVP_PKEY_CTX_set1_hkdf_key(kdf, key_material->data, key_material->len) <= 0
      || EVP_PKEY_derive(kdf, (U8*) out_buf->data, &wrote) <= 0
   )
      GOTO_CLEANUP_CROAK("HKDF failed");
   if (wrote != size)
      GOTO_CLEANUP_CROAK("HKDF returned wrong size");
   out_buf->len= wrote;
   
cleanup:
   if (sv) SvREFCNT_dec(sv);
   if (kdf) EVP_PKEY_CTX_free(kdf);
   if (err) cmk_croak_with_ssl_error(NULL, err);
   return out_buf;
}

/* Simple implementation of HMAC-SHA256 that hashes a combined list of items,
 * each of which can be a plain scalar, scalar-ref, SecretBuffer, or Span.
 */
void
cmk_hmac_sha256(U8 dest[32], const U8 *key, size_t key_len, SV **input, size_t n_items) {
   const char *err = NULL;
   size_t outlen = 0;

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
   /* OpenSSL 3.x: EVP_MAC */
   EVP_MAC *mac = NULL;
   EVP_MAC_CTX *mctx = NULL;

   ERR_clear_error();

   mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
   if (!mac)
      GOTO_CLEANUP_CROAK("EVP_MAC_fetch(HMAC) failed");

   mctx = EVP_MAC_CTX_new(mac);
   if (!mctx)
      GOTO_CLEANUP_CROAK("EVP_MAC_CTX_new failed");

   /* Set digest=SHA256 */
   OSSL_PARAM params[2];
   params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, (char *)"SHA256", 0);
   params[1] = OSSL_PARAM_construct_end();

   if (EVP_MAC_init(mctx, key, key_len, params) != 1)
      GOTO_CLEANUP_CROAK("EVP_MAC_init(HMAC/SHA256) failed");

   for (size_t i = 0; i < n_items; i++) {
      STRLEN len = 0;
      const U8 *buf = (const U8 *)secret_buffer_SvPVbyte(input[i], &len);
      if (len > 0) {
         if (EVP_MAC_update(mctx, buf, (size_t)len) != 1)
            GOTO_CLEANUP_CROAK("EVP_MAC_update failed");
      }
   }

   outlen = 0;
   if (EVP_MAC_final(mctx, dest, &outlen, 32) != 1)
      GOTO_CLEANUP_CROAK("EVP_MAC_final failed");

cleanup:
   if (mctx) EVP_MAC_CTX_free(mctx);
   if (mac) EVP_MAC_free(mac);
#else
   /* OpenSSL 1.1.x: HMAC_CTX (not deprecated there) */
   HMAC_CTX *ctx = NULL;

   ERR_clear_error();

   ctx = HMAC_CTX_new();
   if (!ctx)
      GOTO_CLEANUP_CROAK("HMAC_CTX_new failed");

   if (HMAC_Init_ex(ctx, key, (int)key_len, EVP_sha256(), NULL) != 1)
      GOTO_CLEANUP_CROAK("HMAC_Init_ex(SHA-256) failed");

   for (size_t i = 0; i < n_items; i++) {
      STRLEN len = 0;
      const U8 *buf = (const U8 *)secret_buffer_SvPVbyte(input[i], &len);
      if (len > 0) {
         if (HMAC_Update(ctx, buf, (size_t)len) != 1)
            GOTO_CLEANUP_CROAK("HMAC_Update failed");
      }
   }

   {
      unsigned outlen_u= 0;
      if (HMAC_Final(ctx, dest, &outlen_u) != 1)
         GOTO_CLEANUP_CROAK("HMAC_Final failed");
      outlen= outlen_u;
   }

cleanup:
   if (ctx) HMAC_CTX_free(ctx);
#endif
   if (err) cmk_croak_with_ssl_error("hmac_sha256", err);
   if (outlen != 32)
      croak("hmac_sha256: unexpected mac length %d", (int)outlen);
}

/* Perform symmetric encryption using the supplied key and write ciphertext into ciphertext_out.
 *
 * aes_key must be the correct length for the cipher specified in params.
 *
 * ciphertext_out will be resized as needed, unless you are performing in-place encryption.
 *
 * You can perform in-place encryption by passing ciphertext_out->data as the 'secret' pointer,
 * but with the restriction that ciphertext_out must already have sufficient capacity for the
 * output (which in GCM mode means 30 bytes longer than secret_len), and you can't use the 'pad'
 * feature.
 *
 * In GCM mode, the output of cmk_symmetric_encrypt needs to store at least:
 *   - ciphertext (same length as the secret)
 *   - GCM nonce (12 bytes)
 *   - GCM tag (16 bytes)
 *
 * I didn't find a simple standard for encoding these (without the overhead of ASN.1 and
 * SSL certificates) so I chose a simple concatenated encoding, with the ciphertext placed first
 * so that encryption can be performed in-place on the input buffer before appending the nonce,
 * tag, and flags.
 *
 * I also have an optional "pad" feature which can obfuscate the length of the ciphertext
 * and harden against known-plaintext / structure attacks, even though AES-GCM itself is already
 * secure against known-plaintext attacks when used correctly.
 *
 * I also added an extra 2 bytes of flags at the end, which indicate whether padding was used,
 * and could be used to indicate other encodings in the future.
 *
 * So, the total structure is:
 *
 *   AES-GCM{ data }, 12-byte Nonce, 16-byte GCM Tag, 2-byte Flags
 *
 * or, with padding enabled:
 *
 *   AES-GCM{ 8 byte random header, 0..63 random bytes, 8-byte int64-le data length,
 *              data, NUL byte padding }, Nonce, GCM Tag, Flags
 *
 * In XTS mode, the output is simply N encrypted sectors of data, exactly the same length as
 * the secret.
 */
#define CMK_FLAGS_SIZE 2
#define CMK_FLAG_HAS_PREFIX 1

/* 8 random bytes + 0..63 random bytes + 8 byte secret_len */
#define CMK_PAD_PREFIX_MIN_SIZE (8 + 0 + 8)
#define CMK_PAD_PREFIX_MAX_SIZE (8 + 63 + 8)
#define CMK_PAD_PREFIX_VARSIZE(buf) (((buf)[0] ^ (buf)[1] ^ (buf)[2] ^ (buf)[3] ^ (buf)[4] ^ (buf)[5] ^ (buf)[6] ^ (buf)[7]) & 0x3F)

void cmk_symmetric_encrypt(HV *params, secret_buffer *aes_key, const U8 *secret, size_t secret_len, SV *ciphertext_out) {
   const char *err= NULL;
   const EVP_CIPHER *cipher= NULL;
   EVP_CIPHER_CTX *aes_ctx = NULL;
   SV **svp, *sv= NULL;
   secret_buffer *ciphertext_sb= NULL;
   int outlen, need_key_len, flags= 0;
   STRLEN len;
   bool inplace= false, is_xts= false;
   size_t need_out_len= 0,
          out_capacity= 0,
          prefix_len= 0,
          padded_len= secret_len;
   U8 *buf;
   U8 *out_ptr= NULL;
   U8 prefix_buf[CMK_PAD_PREFIX_MAX_SIZE];

   ERR_clear_error();
   if (!ciphertext_out)
      croak("ciphertext_out must not be NULL");

   /* determine cipher */
   svp= hv_fetchs(params, "cipher", 1);
   if (!svp || !*svp)
      croak("hv_fetchs failed");
   if (SvOK(*svp)) {
      buf= (U8*) SvPVbyte(*svp, len);
   } else {
      len= 11; buf= "AES-256-GCM";
      sv_setpvs(*svp, "AES-256-GCM");
   }
   if (len == 11 && memcmp(buf, "AES-256-GCM", 11) == 0) {
      need_key_len= CMK_AES_256_GCM_KEY_LEN;
      need_out_len= secret_len + CMK_AES_256_GCM_NONCE_LEN + CMK_AES_256_GCM_TAG_LEN + CMK_FLAGS_SIZE;
      cipher= EVP_aes_256_gcm();
   }
   else if (len == 11 && memcmp(buf, "AES-256-XTS", 11) == 0) {
      need_key_len= CMK_AES_256_XTS_KEY_LEN;
      need_out_len= secret_len;
      cipher= EVP_aes_256_xts();
      is_xts= true;
   }
   else
      GOTO_CLEANUP_CROAK("Unsupported cipher");

   if (aes_key->len != (size_t) need_key_len)
      croak("Wrong key length for cipher (%d, need %d)", (int)aes_key->len, need_key_len);

   /* 'pad' feature, for hiding the length of the secret */
   svp= hv_fetchs(params, "pad", 0);
   if (svp && *svp && SvTRUE(*svp)) {
      uint64_t packed_secret_len;
      size_t rem;
      IV pad= SvIV(*svp);
      if (is_xts)
         GOTO_CLEANUP_CROAK("pad feature is incompatible with AES-XTS");
      if (pad < 1 || pad > (1<<30))
         GOTO_CLEANUP_CROAK("pad must be greater than 0 and less/eq than 1GiB (for sanity)");
      flags |= CMK_FLAG_HAS_PREFIX;
      /* generate random prefix bytes */
      if (RAND_bytes(prefix_buf, sizeof(prefix_buf)) != 1)
         GOTO_CLEANUP_CROAK("Failed to generate random prefix");
      /* hash the first 8 bytes to determine how many random bytes to include */
      prefix_len= 8 + CMK_PAD_PREFIX_VARSIZE(prefix_buf) + 8;
      /* now store the true secret length as little-endian 64-bit */
      packed_secret_len= htole64(secret_len);
      memcpy(prefix_buf + prefix_len - 8, &packed_secret_len, 8);
      /* round the whole result up to a multiple of pad */
      padded_len+= prefix_len;
      rem= (prefix_len + need_out_len) % pad;
      if (rem) padded_len+= pad - rem;
      if (padded_len < secret_len)
         GOTO_CLEANUP_CROAK("ciphertext length overflow");
      need_out_len += padded_len - secret_len;
   }
   
   /* Determine output target and detect whether input aliases output.
    * Because we allow the caller to specify source and destination buffers, and the destination
    * buffer will be enlarged automatically, and the caller has the capability of specifying a
    * pointer within the space that could get reallocated, we need to check for that condition.
    */
   if ((ciphertext_sb= secret_buffer_from_magic(ciphertext_out, 0))) {
      out_ptr= (U8*) ciphertext_sb->data;
      if (secret >= out_ptr && secret < out_ptr + ciphertext_sb->capacity) {
         inplace= true;
         if (need_out_len > ciphertext_sb->capacity)
            GOTO_CLEANUP_CROAK("inplace encryption requires preallocated output capacity");
      }
      else if (need_out_len > ciphertext_sb->capacity) {
         secret_buffer_set_len(ciphertext_sb, need_out_len);
         out_ptr= (U8*) ciphertext_sb->data;
      }
   }
   else {
      STRLEN out_capacity;
      if (!SvOK(ciphertext_out))
         sv_setpvs(ciphertext_out, "");
      if (!SvPOK(ciphertext_out) || SvUTF8(ciphertext_out))
         croak("ciphertext_out must be a non-UTF8 byte string scalar or SecretBuffer");
      out_ptr= (U8*) SvPVX(ciphertext_out);
      out_capacity= SvLEN(ciphertext_out) > 0? SvLEN(ciphertext_out)-1 : 0;
      if (secret >= out_ptr && secret < out_ptr + out_capacity) {
         inplace= true;
         if (need_out_len > out_capacity || SvIsCOW(ciphertext_out))
            GOTO_CLEANUP_CROAK("inplace encryption requires preallocated non-shared output capacity");
      }
      else if (need_out_len > out_capacity) {
         /* include NUL terminator and room to convert scalar to CoW if needed */
         sv_grow(ciphertext_out, need_out_len + 1 + sizeof(char*)*2);
         out_ptr= (U8*) SvPVX(ciphertext_out);
      }
   }
   if (inplace && padded_len > secret_len)
      croak("pad feature cannot be encrypted in-place");

   /* AES-XTS operates by performing one encryption for each sector of the input, using the
    * sector number as the initialization vector for that encryption.  That's specific enough
    * that it gets its own setup and loop.
    */
   if (is_xts) {
      size_t sector_size = 512;  /* default to 512-byte sectors for dm-crypt compatibility */
      uint64_t sector_idx = 0;
      size_t num_sectors, sector, offset;

      /* Get XTS sector size if specified */
      svp= hv_fetchs(params, "sector_size", 0);
      if (svp && *svp && SvOK(*svp)) {
         IV sector_size_iv = SvIV(*svp);
         if (sector_size_iv < 16 || sector_size_iv > 1024*1024
            || round_up_to_pow2((size_t)sector_size_iv) != (size_t)sector_size_iv)
            GOTO_CLEANUP_CROAK("sector_size must be a power of 2 between 16 and 1MiB (or between 512 and 4KiB for dm-crypt compat)");
         sector_size = (size_t)sector_size_iv;
      } else {
         /* Store default sector size */
         if (!hv_stores(params, "sector_size", (sv= newSViv(sector_size))))
            GOTO_CLEANUP_CROAK("hv_store");
         sv= NULL;
      }
      num_sectors= secret_len / sector_size;
      if (num_sectors * sector_size != secret_len)
         GOTO_CLEANUP_CROAK("secret must be an even multiple of sector_size");

      /* get index of starting sector */
      svp= hv_fetchs(params, "sector_idx", 0);
      if (svp && *svp && SvOK(*svp)) {
         IV sector_idx_iv = SvIV(*svp);
         if (sector_idx_iv < 0)
            GOTO_CLEANUP_CROAK("sector_idx must be non-negative");
         sector_idx = (uint64_t)sector_idx_iv;
      }

      if (!(aes_ctx = EVP_CIPHER_CTX_new()))
         GOTO_CLEANUP_CROAK("AES context creation failed");

      offset= 0;
      for (sector = 0; sector < num_sectors; offset += sector_size, sector++) {
         /* XTS uses the sector number as the 'tweak' value */
         uint64_t tweak[2]= { htole64(sector_idx + sector), 0 };

         if (EVP_EncryptInit_ex(aes_ctx, cipher, NULL, aes_key->data, (U8*)tweak) != 1)
            GOTO_CLEANUP_CROAK("AES-XTS init failed");

         if (EVP_EncryptUpdate(aes_ctx, out_ptr + offset, &outlen, 
                               secret + offset, (int)sector_size) != 1
            || (size_t)outlen != sector_size)
            GOTO_CLEANUP_CROAK("AES-XTS encrypt failed");

         if (EVP_EncryptFinal_ex(aes_ctx, NULL, &outlen) != 1)
            GOTO_CLEANUP_CROAK("AES-XTS final failed");
      }
   }
   else { /* not -XTS */
      U8 *ciphertext_pos, *ciphertext_lim, *nonce, *gcm_tag, *flags_p;
      const U8 *secret_pos, *secret_lim;

      ciphertext_pos= out_ptr;
      ciphertext_lim= out_ptr + padded_len;
      nonce= ciphertext_lim;
      gcm_tag= nonce + CMK_AES_256_GCM_NONCE_LEN;
      flags_p= gcm_tag + CMK_AES_256_GCM_TAG_LEN;
      
      ASSUME(flags_p + CMK_FLAGS_SIZE == out_ptr + need_out_len);

      if (RAND_bytes(nonce, CMK_AES_256_GCM_NONCE_LEN) != 1)
         GOTO_CLEANUP_CROAK("Failed to generate GCM nonce");

      if (!(aes_ctx = EVP_CIPHER_CTX_new())
         || EVP_EncryptInit_ex(aes_ctx, cipher, NULL, NULL, NULL) != 1
         || EVP_CIPHER_CTX_ctrl(aes_ctx, EVP_CTRL_GCM_SET_IVLEN, CMK_AES_256_GCM_NONCE_LEN, NULL) != 1
         || EVP_EncryptInit_ex(aes_ctx, NULL, NULL, (U8*) aes_key->data, nonce) != 1
      )
         GOTO_CLEANUP_CROAK("AES-GCM init failed");

      /* The auth_data parameter allows the user to include AAD
       * (Additional Authenticated Data) into the GCM counter so that the GCM tag is
       * validating the authenticity of the secret *and* the additional data, even
       * though the additional data is not included in the ciphertext.
       */
      svp= hv_fetchs(params, "auth_data", 0);
      if (svp && *svp && SvOK(*svp)) {
         STRLEN aad_len = 0;
         const U8 *aad = (const U8*)secret_buffer_SvPVbyte(*svp, &aad_len);
         if (aad_len) {
            if (EVP_EncryptUpdate(aes_ctx, NULL, &outlen, aad, (int)aad_len) != 1)
               GOTO_CLEANUP_CROAK("AES-GCM add auth_data failed");
         }
      }

      if (prefix_len) {
         if (EVP_EncryptUpdate(aes_ctx, ciphertext_pos, &outlen, prefix_buf, (int)prefix_len) != 1
            || outlen != (int)prefix_len)
            GOTO_CLEANUP_CROAK("AES-GCM encrypt (secret_len) failed");
         ciphertext_pos += prefix_len;
      }

      for (secret_pos= secret, secret_lim= secret+secret_len; secret_pos < secret_lim;) {
         /* handle unlikely edge case where secret is larger than INT_MAX */
         size_t remain= secret_lim - secret_pos;
         int n= remain > INT_MAX? INT_MAX : (int) remain;
         if (EVP_EncryptUpdate(aes_ctx, ciphertext_pos, &outlen, secret_pos, n) != 1
            || outlen != n)
            GOTO_CLEANUP_CROAK("AES-GCM encrypt (secret) failed");
         secret_pos += n;
         ciphertext_pos += n;
      }

      /* Encrypt empty bytes up to requested length */
      if (padded_len > secret_len) {
         U8 tmp[1024];
         memset(tmp, 0, sizeof(tmp));
         while (ciphertext_pos < ciphertext_lim) {
            int chunk= ciphertext_lim - ciphertext_pos;
            if (chunk > sizeof(tmp))
               chunk= sizeof(tmp);
            if (EVP_EncryptUpdate(aes_ctx, ciphertext_pos, &outlen, tmp, chunk) != 1
               || outlen != chunk)
               GOTO_CLEANUP_CROAK("AES-GCM encrypt (padding) failed");
            ciphertext_pos += chunk;
         }
      }

      if (EVP_EncryptFinal_ex(aes_ctx, NULL, &outlen) != 1)
         GOTO_CLEANUP_CROAK("AES-GCM final failed");

      if (EVP_CIPHER_CTX_ctrl(aes_ctx, EVP_CTRL_GCM_GET_TAG, CMK_AES_256_GCM_TAG_LEN, gcm_tag) != 1)
         GOTO_CLEANUP_CROAK("AES-GCM get tag failed");

      /* write flags */
      flags_p[0]= (U8)(flags & 0xFF);
      flags_p[1]= (U8)(flags >> 8);
   }
   /* Update output container official length, delayed in case input and output buffers overlap.
    * Capacity was ensured earlier. */
   if (ciphertext_sb) {
      secret_buffer_set_len(ciphertext_sb, need_out_len);
   } else {
      SvCUR_set(ciphertext_out, need_out_len);
      *SvEND(ciphertext_out)= '\0';
   }
cleanup:
   OPENSSL_cleanse(prefix_buf, sizeof(prefix_buf));
   if (sv) SvREFCNT_dec(sv);
   if (aes_ctx) EVP_CIPHER_CTX_free(aes_ctx);
   if (err) cmk_croak_with_ssl_error("symmetric_encrypt", err);
}

/* Perform symmetric decryption using the supplied AES key and ciphertext and parameters,
 * storing the original secret into secret_out.
 *
 * Like cmk_symmetric_encrypt, this can perform an in-place decryption by passing the secret_out
 * data pointer as the `ciphertext` pointer, but restricted to XTS mode or GCM without the 'pad'
 * feature.
 */
void cmk_symmetric_decrypt(HV *params, secret_buffer *aes_key, const U8 *ciphertext, size_t ciphertext_len, secret_buffer *secret_out) {
   const char *err= NULL;
   const EVP_CIPHER *cipher= NULL;
   EVP_CIPHER_CTX *aes_ctx = NULL;
   int outlen, final_len, need_key_len;
   bool is_xts= false;
   U8 prefix_buf[CMK_PAD_PREFIX_MAX_SIZE];
   size_t prefix_len;
   /* check for overlap between input and output */
   bool inplace= secret_out->capacity && ciphertext >= (U8*) secret_out->data
      && ciphertext < (U8*) secret_out->data + secret_out->capacity;
   STRLEN len;
   U8 *buf;
   SV **svp;

   ERR_clear_error();

   /* Determine cipher */
   svp= hv_fetchs(params, "cipher", 0);
   if (!svp || !*svp || !SvOK(*svp))
      croak("Missing 'cipher'");
   buf= (U8*) secret_buffer_SvPVbyte(*svp, &len);
   if (len == 11 && memcmp(buf, "AES-256-GCM", 11) == 0) {
      need_key_len= CMK_AES_256_GCM_KEY_LEN;
      cipher= EVP_aes_256_gcm();
   }
   else if (len == 11 && memcmp(buf, "AES-256-XTS", 11) == 0) {
      need_key_len= CMK_AES_256_XTS_KEY_LEN;
      cipher= EVP_aes_256_xts();
      is_xts= true;
   }
   else
      croak("Unsupported cipher");

   if (aes_key->len != (size_t) need_key_len)
      croak("Wrong key length for cipher (%d, need %d)", (int)aes_key->len, need_key_len);

   /* AES-XTS operates by performing one encryption for each sector of the input, using the
    * sector number as the initialization vector for that encryption.  That's specific enough
    * that it gets its own setup and loop.
    */
   if (is_xts) {
      size_t sector_size = 512;  /* default to 512-byte sectors for dm-crypt compatibility */
      uint64_t sector_idx= 0;
      size_t num_sectors, sector, offset;

      /* Get XTS sector size if not default of 512 */
      svp= hv_fetchs(params, "sector_size", 0);
      if (svp && *svp && SvOK(*svp)) {
         IV sector_size_iv = SvIV(*svp);
         if (sector_size_iv < 16 || sector_size_iv > 1024*1024
            || round_up_to_pow2((size_t)sector_size_iv) != (size_t)sector_size_iv)
            GOTO_CLEANUP_CROAK("sector_size must be a power of 2 between 16 and 1MiB");
         sector_size = (size_t)sector_size_iv;
      }

      num_sectors= ciphertext_len / sector_size;
      if (num_sectors * sector_size != ciphertext_len)
         GOTO_CLEANUP_CROAK("ciphertext must be an even multiple of sector_size");
      /* if doing inplace decryption, the secretbuffer will already need to be large enough,
         so this won't be a problem. */
      if (secret_out->capacity < ciphertext_len)
         secret_buffer_alloc_at_least(secret_out, ciphertext_len);

      /* get sector index */
      svp= hv_fetchs(params, "sector_idx", 0);
      if (svp && *svp && SvOK(*svp)) {
         IV sector_idx_iv = SvIV(*svp);
         if (sector_idx_iv < 0)
            GOTO_CLEANUP_CROAK("sector_idx must be non-negative");
         sector_idx = (uint64_t)sector_idx_iv;
      }

      if (!(aes_ctx = EVP_CIPHER_CTX_new()))
         GOTO_CLEANUP_CROAK("AES-XTS context creation failed");

      offset= 0;
      for (sector = 0; sector < num_sectors; offset += sector_size, sector++) {
         /* XTS uses the sector number as the 'tweak' value */
         uint64_t tweak[2]= { htole64(sector_idx + sector), 0 };

         if (EVP_DecryptInit_ex(aes_ctx, cipher, NULL, aes_key->data, (U8*)tweak) != 1)
            GOTO_CLEANUP_CROAK("AES-XTS decrypt init failed");

         if (EVP_DecryptUpdate(aes_ctx, secret_out->data + offset, &outlen,
                               ciphertext + offset, (int)sector_size) != 1
            || (size_t)outlen != sector_size)
            GOTO_CLEANUP_CROAK("AES-XTS decrypt failed");

         if (EVP_DecryptFinal_ex(aes_ctx, NULL, &final_len) != 1)
            GOTO_CLEANUP_CROAK("AES-XTS decrypt final failed");
      }
      /* capacity was already ensured, now update official length */
      secret_buffer_set_len(secret_out, ciphertext_len);
   }
   else { /* not -XTS */
      const U8 *ciphertext_pos, *ciphertext_lim, *nonce, *gcm_tag;
      U8 *secret_pos, *secret_lim;
      int flags;
      size_t secret_len;

      if (ciphertext_len < CMK_AES_256_GCM_NONCE_LEN + CMK_AES_256_GCM_TAG_LEN + CMK_FLAGS_SIZE)
         croak("ciphertext length (%ld) smaller than minimum GCM nonce+tag +flags", (long)ciphertext_len);
      flags= (ciphertext[ciphertext_len-1] << 8) | ciphertext[ciphertext_len-2];
      ciphertext_len -= CMK_AES_256_GCM_NONCE_LEN + CMK_AES_256_GCM_TAG_LEN + CMK_FLAGS_SIZE;
      secret_len= ciphertext_len;
      ciphertext_pos= ciphertext;
      ciphertext_lim= ciphertext + ciphertext_len;
      nonce= ciphertext_lim;
      gcm_tag= nonce + CMK_AES_256_GCM_NONCE_LEN;

      if (!(aes_ctx = EVP_CIPHER_CTX_new())
         || EVP_DecryptInit_ex(aes_ctx, cipher, NULL, NULL, NULL) != 1
         || EVP_CIPHER_CTX_ctrl(aes_ctx, EVP_CTRL_GCM_SET_IVLEN, CMK_AES_256_GCM_NONCE_LEN, NULL) != 1
         || EVP_DecryptInit_ex(aes_ctx, NULL, NULL, (U8*) aes_key->data, nonce) != 1
         || EVP_CIPHER_CTX_ctrl(aes_ctx, EVP_CTRL_GCM_SET_TAG, CMK_AES_256_GCM_TAG_LEN, (U8*) gcm_tag) != 1
      )
         GOTO_CLEANUP_CROAK("AES-GCM decrypt init failed");

      /* The auth_data parameter allows the user to include AAD
       * (Additional Authenticated Data) into the GCM counter so that the GCM tag is
       * validating the authenticity of the secret *and* the additional data, even
       * though the additional data is not included in the ciphertext.
       */
      svp = hv_fetchs(params, "auth_data", 0);
      if (svp && *svp && SvOK(*svp)) {
         STRLEN aad_len = 0;
         const U8 *aad = (const U8*)secret_buffer_SvPVbyte(*svp, &aad_len);
         if (aad_len) {
            if (EVP_DecryptUpdate(aes_ctx, NULL, &outlen, aad, (int)aad_len) != 1)
               GOTO_CLEANUP_CROAK("AES-GCM add auth_data failed");
         }
      }
      /* check if length obfuscation was used */
      if (flags & CMK_FLAG_HAS_PREFIX) {
         uint64_t packed_secret_len;
         if (inplace)
            croak("pad feature cannot be decrypted in-place");
         /* read up to PREFIX_MAX_SIZE of potential prefix */
         len= ciphertext_len < sizeof(prefix_buf)? ciphertext_len : sizeof(prefix_buf);
         if (len < CMK_PAD_PREFIX_MIN_SIZE)
            GOTO_CLEANUP_CROAK("Ciphertext is too short to be a padded secret");
         if (EVP_DecryptUpdate(aes_ctx, prefix_buf, &outlen, ciphertext_pos, (int)len) != 1)
            GOTO_CLEANUP_CROAK("AES-GCM decrypt failed");
         /* hash the first 8 bytes to determine true length of the prefix */
         prefix_len= 8 + CMK_PAD_PREFIX_VARSIZE(prefix_buf) + 8;
         if (len < prefix_len)
            GOTO_CLEANUP_CROAK("Ciphertext is too short to be a padded secret");
         /* now load the true secret length as little-endian 64-bit */
         memcpy(&packed_secret_len, prefix_buf + prefix_len - 8, 8);
         secret_len= le64toh(packed_secret_len);
         /* sanity check.  secret_len could be INT_MAX etc, so guard against overflow */
         if (secret_len > ciphertext_len || prefix_len > ciphertext_len - secret_len)
            GOTO_CLEANUP_CROAK("Ciphertext is too short to be a padded secret");
         ciphertext_pos += outlen;
         /* output data inside prefix_buf gets copied below, after reallocating output buffer */
      }
      /* allocate secret buffer and set up position pointers */
      if (secret_len > secret_out->capacity) {
         if (inplace)
            GOTO_CLEANUP_CROAK("inplace decryption requires sufficient output capacity");
         secret_buffer_alloc_at_least(secret_out, secret_len);
      }
      /* ensure sb wipes anything we write beyond the current ->len by growing len,
       * but don't shrink len yet, or it would break inplace. */
      if (secret_len > secret_out->len)
         secret_out->len= secret_len;
      secret_pos= (U8*) secret_out->data;
      secret_lim= secret_pos + secret_len;
      /* copy across any bytes that were already decrypted and not part of the prefix */
      if ((flags & CMK_FLAG_HAS_PREFIX) && outlen > prefix_len) {
         size_t n= outlen - prefix_len;
         if (n > secret_len)
            n= secret_len;
         memcpy(secret_pos, prefix_buf+prefix_len, n);
         secret_pos += n;
      }
      while (ciphertext_pos < ciphertext_lim) {
         if (secret_pos < secret_lim) {
            /* handle unlikely edge case that buffer size could exceed INT_MAX */
            int n= secret_lim - secret_pos > INT_MAX? INT_MAX : (int)(secret_lim - secret_pos);
            if (EVP_DecryptUpdate(aes_ctx, secret_pos, &outlen, ciphertext_pos, n) != 1
               || outlen != n)
               GOTO_CLEANUP_CROAK("AES-GCM decrypt failed");
            secret_pos += n;
            ciphertext_pos += n;
         }
         else { /* Decrypt the remainder of padding which should all be NUL bytes */
            U8 buffer[1024];
            int n= (ciphertext_lim - ciphertext_pos) > sizeof(buffer)? sizeof(buffer) : (int)(ciphertext_lim - ciphertext_pos);
            if (EVP_DecryptUpdate(aes_ctx, buffer, &outlen, ciphertext_pos, n) != 1
               || outlen != n)
               GOTO_CLEANUP_CROAK("AES-GCM decrypt failed");
            ciphertext_pos += n;
         }
      }
      ASSUME(secret_pos == secret_lim);

      /* Finalize and verify tag */
      final_len = 0;
      if (EVP_DecryptFinal_ex(aes_ctx, NULL, &final_len) != 1
         || final_len != 0)
         GOTO_CLEANUP_CROAK("AES-GCM decrypt failed, gcm_tag mismatch");
      /* capacity was ensured earlier */
      secret_buffer_set_len(secret_out, secret_len);
   }
cleanup:
   OPENSSL_cleanse(prefix_buf, sizeof(prefix_buf));
   if (aes_ctx) EVP_CIPHER_CTX_free(aes_ctx);
   if (err) {
      secret_buffer_set_len(secret_out, 0); /* calls OPENSSL_cleanse */
      cmk_croak_with_ssl_error("symmetric_decrypt", err);
   }
}

#include "cmk_parse_openssh.c"
