
/*
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) F5, Inc.
 */


#include <qjs.h>
#include <njs_sprintf.h>
#include "njs_openssl.h"

typedef enum {
    QJS_KEY_FORMAT_RAW          = 1 << 1,
    QJS_KEY_FORMAT_PKCS8        = 1 << 2,
    QJS_KEY_FORMAT_SPKI         = 1 << 3,
    QJS_KEY_FORMAT_JWK          = 1 << 4,
    QJS_KEY_FORMAT_UNKNOWN      = 1 << 5,
} qjs_webcrypto_key_format_t;


typedef enum {
    QJS_KEY_JWK_KTY_RSA,
    QJS_KEY_JWK_KTY_EC,
    QJS_KEY_JWK_KTY_OCT,
    QJS_KEY_JWK_KTY_OKP,
    QJS_KEY_JWK_KTY_UNKNOWN,
} qjs_webcrypto_jwk_kty_t;


typedef enum {
    QJS_KEY_USAGE_DECRYPT       = 1 << 1,
    QJS_KEY_USAGE_DERIVE_BITS   = 1 << 2,
    QJS_KEY_USAGE_DERIVE_KEY    = 1 << 3,
    QJS_KEY_USAGE_ENCRYPT       = 1 << 4,
    QJS_KEY_USAGE_GENERATE_KEY  = 1 << 5,
    QJS_KEY_USAGE_SIGN          = 1 << 6,
    QJS_KEY_USAGE_VERIFY        = 1 << 7,
    QJS_KEY_USAGE_WRAP_KEY      = 1 << 8,
    QJS_KEY_USAGE_UNSUPPORTED   = 1 << 9,
    QJS_KEY_USAGE_UNWRAP_KEY    = 1 << 10,
} qjs_webcrypto_key_usage_t;


typedef enum {
    QJS_ALGORITHM_RSASSA_PKCS1_v1_5 = 0,
    QJS_ALGORITHM_RSA_PSS,
    QJS_ALGORITHM_RSA_OAEP,
    QJS_ALGORITHM_HMAC,
    QJS_ALGORITHM_AES_GCM,
    QJS_ALGORITHM_AES_CTR,
    QJS_ALGORITHM_AES_CBC,
    QJS_ALGORITHM_AES_KW,
    QJS_ALGORITHM_ECDSA,
    QJS_ALGORITHM_ECDH,
    QJS_ALGORITHM_ED25519,
    QJS_ALGORITHM_X25519,
    QJS_ALGORITHM_PBKDF2,
    QJS_ALGORITHM_HKDF,
    QJS_ALGORITHM_MAX,
} qjs_webcrypto_alg_t;


typedef enum {
    QJS_HASH_UNSET = 0,
    QJS_HASH_SHA1,
    QJS_HASH_SHA256,
    QJS_HASH_SHA384,
    QJS_HASH_SHA512,
    QJS_HASH_MAX,
} qjs_webcrypto_hash_t;


typedef struct {
    njs_str_t                  name;
    uintptr_t                  value;
} qjs_webcrypto_entry_t;


typedef struct {
    qjs_webcrypto_alg_t        type;
    unsigned                   usage;
    unsigned                   fmt;
    unsigned                   raw;
} qjs_webcrypto_algorithm_t;


typedef struct {
    qjs_webcrypto_algorithm_t  *alg;
    unsigned                   usage;
    int                        extractable;

    qjs_webcrypto_hash_t       hash;

    union {
        struct {
            EVP_PKEY          *pkey;
            int               privat;
            int               curve;
        } a;
        struct {
            njs_str_t         raw;
        } s;
    } u;

} qjs_webcrypto_key_t;


typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX *ctx);
typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX *ctx, unsigned char *out,
    size_t *outlen, const unsigned char *in, size_t inlen);

static JSValue qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv, int encrypt);
static JSValue qjs_cipher_pkey(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, int encrypt);
static JSValue qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, JSValue options, int encrypt);
static JSValue qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, JSValue options, int encrypt);
static JSValue qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, JSValue options, int encrypt);
#if (NJS_HAVE_AES_WRAP)
static JSValue qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, int encrypt);
#endif
static JSValue qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc,
    int derive_key, qjs_webcrypto_key_t *key);
static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv, int derive_key);
static JSValue qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
#if (NJS_HAVE_ED25519)
static int qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
    qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg,
    unsigned usage, int extractable, unsigned priv_usage,
    unsigned pub_usage, JSValue *key, JSValue *obj_ret);
#endif
static JSValue qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_webcrypto_import_key_internal(JSContext *cx,
    qjs_webcrypto_key_format_t fmt, JSValueConst key_data,
    qjs_webcrypto_algorithm_t *alg, JSValueConst options,
    int extractable, unsigned usage);
static JSValue qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv, int verify);
static JSValue qjs_webcrypto_wrap_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_webcrypto_unwrap_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);

static JSValue qjs_webcrypto_key_algorithm(JSContext *cx,
    JSValueConst this_val);
static JSValue qjs_webcrypto_key_extractable(JSContext *cx,
    JSValueConst this_val);
static JSValue qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val);
static JSValue qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val);

static JSValue qjs_webcrypto_export_key_raw(JSContext *cx,
    qjs_webcrypto_key_t *key, qjs_webcrypto_key_format_t fmt);
static JSValue qjs_webcrypto_cipher_core(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, JSValue options,
    qjs_webcrypto_algorithm_t *alg, int encrypt);
static JSValue qjs_get_random_values(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_random_uuid(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv);

static JSValue qjs_webcrypto_key_make(JSContext *cx,
  qjs_webcrypto_algorithm_t *alg, unsigned usage, int extractable);
static void qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val);

static qjs_webcrypto_key_format_t qjs_key_format(JSContext *cx,
    JSValueConst value);
static qjs_webcrypto_jwk_kty_t qjs_jwk_kty(JSContext *cx, JSValueConst value);
static qjs_webcrypto_algorithm_t *qjs_key_algorithm(JSContext *cx, JSValue val);
static JSValue qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve);
static JSValue qjs_algorithm_hash(JSContext *cx, JSValue options,
    qjs_webcrypto_hash_t *hash);
static const EVP_MD *qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash);
static njs_str_t *qjs_algorithm_curve_name(int curve);
static const char *qjs_format_string(qjs_webcrypto_key_format_t fmt);
static const char *qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm);
static const char *qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash);
static JSValue qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask);
static JSValue qjs_key_ops(JSContext *cx, unsigned mask);
static void qjs_webcrypto_error(JSContext *cx, const char *fmt, ...);

static JSModuleDef *qjs_webcrypto_init(JSContext *cx, const char *name);


static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = {

#define qjs_webcrypto_algorithm(type, usage, fmt, raw)                       \
    (uintptr_t) & (qjs_webcrypto_algorithm_t) { type, usage, fmt, raw }

    {
      njs_str("RSASSA-PKCS1-v1_5"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSASSA_PKCS1_v1_5,
                              QJS_KEY_USAGE_SIGN |
                              QJS_KEY_USAGE_VERIFY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

    {
      njs_str("RSA-PSS"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_PSS,
                              QJS_KEY_USAGE_SIGN |
                              QJS_KEY_USAGE_VERIFY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

    {
      njs_str("RSA-OAEP"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_OAEP,
                              QJS_KEY_USAGE_ENCRYPT |
                              QJS_KEY_USAGE_DECRYPT |
                              QJS_KEY_USAGE_WRAP_KEY |
                              QJS_KEY_USAGE_UNWRAP_KEY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

    {
      njs_str("HMAC"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_HMAC,
                              QJS_KEY_USAGE_GENERATE_KEY |
                              QJS_KEY_USAGE_SIGN |
                              QJS_KEY_USAGE_VERIFY,
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              1)
    },

    {
      njs_str("AES-GCM"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_GCM,
                              QJS_KEY_USAGE_ENCRYPT |
                              QJS_KEY_USAGE_DECRYPT |
                              QJS_KEY_USAGE_WRAP_KEY |
                              QJS_KEY_USAGE_UNWRAP_KEY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              1)
    },

    {
      njs_str("AES-CTR"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CTR,
                              QJS_KEY_USAGE_ENCRYPT |
                              QJS_KEY_USAGE_DECRYPT |
                              QJS_KEY_USAGE_WRAP_KEY |
                              QJS_KEY_USAGE_UNWRAP_KEY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              1)
    },

    {
      njs_str("AES-CBC"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CBC,
                              QJS_KEY_USAGE_ENCRYPT |
                              QJS_KEY_USAGE_DECRYPT |
                              QJS_KEY_USAGE_WRAP_KEY |
                              QJS_KEY_USAGE_UNWRAP_KEY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              1)
    },

#if (NJS_HAVE_AES_WRAP)
    {
      njs_str("AES-KW"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_KW,
                              QJS_KEY_USAGE_WRAP_KEY |
                              QJS_KEY_USAGE_UNWRAP_KEY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              1)
    },
#endif

    {
      njs_str("ECDSA"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDSA,
                              QJS_KEY_USAGE_SIGN |
                              QJS_KEY_USAGE_VERIFY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

    {
      njs_str("ECDH"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDH,
                              QJS_KEY_USAGE_DERIVE_KEY |
                              QJS_KEY_USAGE_DERIVE_BITS |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

#if (NJS_HAVE_ED25519)
    {
      njs_str("Ed25519"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_ED25519,
                              QJS_KEY_USAGE_SIGN |
                              QJS_KEY_USAGE_VERIFY |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },

    {
      njs_str("X25519"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_X25519,
                              QJS_KEY_USAGE_DERIVE_KEY |
                              QJS_KEY_USAGE_DERIVE_BITS |
                              QJS_KEY_USAGE_GENERATE_KEY,
                              QJS_KEY_FORMAT_PKCS8 |
                              QJS_KEY_FORMAT_SPKI |
                              QJS_KEY_FORMAT_RAW |
                              QJS_KEY_FORMAT_JWK,
                              0)
    },
#endif

    {
      njs_str("PBKDF2"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_PBKDF2,
                              QJS_KEY_USAGE_DERIVE_KEY |
                              QJS_KEY_USAGE_DERIVE_BITS,
                              QJS_KEY_FORMAT_RAW,
                              1)
    },

    {
      njs_str("HKDF"),
      qjs_webcrypto_algorithm(QJS_ALGORITHM_HKDF,
                              QJS_KEY_USAGE_DERIVE_KEY |
                              QJS_KEY_USAGE_DERIVE_BITS,
                              QJS_KEY_FORMAT_RAW,
                              1)
    },

    {
        njs_null_str,
        0
    }
};


static qjs_webcrypto_entry_t qjs_webcrypto_hash[] = {
    { njs_str("SHA-256"), QJS_HASH_SHA256 },
    { njs_str("SHA-384"), QJS_HASH_SHA384 },
    { njs_str("SHA-512"), QJS_HASH_SHA512 },
    { njs_str("SHA-1"), QJS_HASH_SHA1 },
    { njs_null_str, 0 }
};


static qjs_webcrypto_entry_t qjs_webcrypto_curve[] = {
    { njs_str("P-256"), NID_X9_62_prime256v1 },
    { njs_str("P-384"), NID_secp384r1 },
    { njs_str("P-521"), NID_secp521r1 },
    { njs_null_str, 0 }
};


static qjs_webcrypto_entry_t qjs_webcrypto_format[] = {
    { njs_str("raw"), QJS_KEY_FORMAT_RAW },
    { njs_str("pkcs8"), QJS_KEY_FORMAT_PKCS8 },
    { njs_str("spki"), QJS_KEY_FORMAT_SPKI },
    { njs_str("jwk"), QJS_KEY_FORMAT_JWK },
    { njs_null_str, QJS_KEY_FORMAT_UNKNOWN }
};


static qjs_webcrypto_entry_t qjs_webcrypto_jwk_kty[] = {
    { njs_str("RSA"), QJS_KEY_JWK_KTY_RSA },
    { njs_str("EC"), QJS_KEY_JWK_KTY_EC },
    { njs_str("oct"), QJS_KEY_JWK_KTY_OCT },
    { njs_str("OKP"), QJS_KEY_JWK_KTY_OKP },
    { njs_null_str, QJS_KEY_JWK_KTY_UNKNOWN }
};


static qjs_webcrypto_entry_t qjs_webcrypto_usage[] = {
    { njs_str("decrypt"), QJS_KEY_USAGE_DECRYPT },
    { njs_str("deriveBits"), QJS_KEY_USAGE_DERIVE_BITS },
    { njs_str("deriveKey"), QJS_KEY_USAGE_DERIVE_KEY },
    { njs_str("encrypt"), QJS_KEY_USAGE_ENCRYPT },
    { njs_str("sign"), QJS_KEY_USAGE_SIGN },
    { njs_str("unwrapKey"), QJS_KEY_USAGE_UNWRAP_KEY },
    { njs_str("verify"), QJS_KEY_USAGE_VERIFY },
    { njs_str("wrapKey"), QJS_KEY_USAGE_WRAP_KEY },
    { njs_null_str, 0 }
};


static qjs_webcrypto_entry_t qjs_webcrypto_alg_hash[] = {
    { njs_str("RS1"), QJS_HASH_SHA1 },
    { njs_str("RS256"), QJS_HASH_SHA256 },
    { njs_str("RS384"), QJS_HASH_SHA384 },
    { njs_str("RS512"), QJS_HASH_SHA512 },
    { njs_str("PS1"), QJS_HASH_SHA1 },
    { njs_str("PS256"), QJS_HASH_SHA256 },
    { njs_str("PS384"), QJS_HASH_SHA384 },
    { njs_str("PS512"), QJS_HASH_SHA512 },
    { njs_str("RSA-OAEP"), QJS_HASH_SHA1 },
    { njs_str("RSA-OAEP-256"), QJS_HASH_SHA256 },
    { njs_str("RSA-OAEP-384"), QJS_HASH_SHA384 },
    { njs_str("RSA-OAEP-512"), QJS_HASH_SHA512 },
    { njs_null_str, 0 }
};


static njs_str_t
    qjs_webcrypto_alg_name[QJS_ALGORITHM_HMAC + 1][QJS_HASH_MAX] = {
    {
        njs_null_str,
        njs_str("RS1"),
        njs_str("RS256"),
        njs_str("RS384"),
        njs_str("RS512"),
    },

    {
        njs_null_str,
        njs_str("PS1"),
        njs_str("PS256"),
        njs_str("PS384"),
        njs_str("PS512"),
    },

    {
        njs_null_str,
        njs_str("RSA-OAEP"),
        njs_str("RSA-OAEP-256"),
        njs_str("RSA-OAEP-384"),
        njs_str("RSA-OAEP-512"),
    },

    {
        njs_null_str,
        njs_str("HS1"),
        njs_str("HS256"),
        njs_str("HS384"),
        njs_str("HS512"),
    },
};

static njs_str_t qjs_webcrypto_alg_aes_name[4][3 + 1] = {
    {
        njs_str("A128GCM"),
        njs_str("A192GCM"),
        njs_str("A256GCM"),
        njs_null_str,
    },

    {
        njs_str("A128CTR"),
        njs_str("A192CTR"),
        njs_str("A256CTR"),
        njs_null_str,
    },

    {
        njs_str("A128CBC"),
        njs_str("A192CBC"),
        njs_str("A256CBC"),
        njs_null_str,
    },

    {
        njs_str("A128KW"),
        njs_str("A192KW"),
        njs_str("A256KW"),
        njs_null_str,
    },
};


static const JSCFunctionListEntry qjs_webcrypto_subtle[] = {
    JS_CFUNC_DEF("importKey", 5, qjs_webcrypto_import_key),
    JS_CFUNC_MAGIC_DEF("decrypt", 4, qjs_webcrypto_cipher, 0),
    JS_CFUNC_MAGIC_DEF("deriveBits", 4, qjs_webcrypto_derive, 0),
    JS_CFUNC_MAGIC_DEF("deriveKey", 5, qjs_webcrypto_derive, 1),
    JS_CFUNC_DEF("digest", 3, qjs_webcrypto_digest),
    JS_CFUNC_MAGIC_DEF("encrypt", 4, qjs_webcrypto_cipher, 1),
    JS_CFUNC_DEF("exportKey", 3, qjs_webcrypto_export_key),
    JS_CFUNC_DEF("generateKey", 3, qjs_webcrypto_generate_key),
    JS_CFUNC_MAGIC_DEF("sign", 4, qjs_webcrypto_sign, 0),
    JS_CFUNC_DEF("unwrapKey", 7, qjs_webcrypto_unwrap_key),
    JS_CFUNC_MAGIC_DEF("verify", 4, qjs_webcrypto_sign, 1),
    JS_CFUNC_DEF("wrapKey", 4, qjs_webcrypto_wrap_key),
};


static const JSCFunctionListEntry qjs_webcrypto_key_proto[] = {
    JS_PROP_STRING_DEF("[Symbol.toStringTag]", "CryptoKey",
                       JS_PROP_CONFIGURABLE),
    JS_CGETSET_DEF("algorithm", qjs_webcrypto_key_algorithm, NULL),
    JS_CGETSET_DEF("extractable", qjs_webcrypto_key_extractable, NULL),
    JS_CGETSET_DEF("type", qjs_webcrypto_key_type, NULL),
    JS_CGETSET_DEF("usages", qjs_webcrypto_key_usages, NULL),
};


static const JSCFunctionListEntry qjs_webcrypto_export[] = {
    JS_CFUNC_DEF("getRandomValues", 1, qjs_get_random_values),
    JS_CFUNC_DEF("randomUUID", 0, qjs_random_uuid),
    JS_OBJECT_DEF("subtle",
                  qjs_webcrypto_subtle,
                  njs_nitems(qjs_webcrypto_subtle),
                  JS_PROP_CONFIGURABLE),
};


static JSClassDef qjs_webcrypto_key_class = {
    "WebCryptoKey",
    .finalizer = qjs_webcrypto_key_finalizer,
};


qjs_module_t  qjs_webcrypto_module = {
    .name = "webcrypto",
    .init = qjs_webcrypto_init,
};


static JSValue
qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv, int encrypt)
{
    unsigned                   mask;
    JSValue                    ret, options;
    njs_str_t                  data;
    qjs_webcrypto_key_t        *key;
    qjs_webcrypto_algorithm_t  *alg;

    options = argv[0];
    alg = qjs_key_algorithm(cx, options);
    if (alg == NULL) {
        goto fail;
    }

    key = JS_GetOpaque(argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "key is not a CryptoKey object");
        goto fail;
    }

    mask = encrypt ? QJS_KEY_USAGE_ENCRYPT : QJS_KEY_USAGE_DECRYPT;

    if (alg->type == QJS_ALGORITHM_AES_KW) {
        mask = encrypt ? QJS_KEY_USAGE_WRAP_KEY : QJS_KEY_USAGE_UNWRAP_KEY;
    }

    if ((key->usage & mask) != mask) {
        JS_ThrowTypeError(cx, "key does not support %s operation",
                          encrypt ? "encrypt" : "decrypt");
        goto fail;
    }

    if (key->alg != alg) {
        JS_ThrowTypeError(cx, "cannot %s use \"%s\" with \"%s\" key",
                          encrypt ? "encrypt" : "decrypt",
                          qjs_algorithm_string(key->alg),
                          qjs_algorithm_string(alg));
        goto fail;
    }

    ret = qjs_typed_array_data(cx, argv[2], &data);
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_webcrypto_cipher_core(cx, &data, key, options, alg, encrypt);
    if (JS_IsException(ret)) {
        goto fail;
    }

    return qjs_promise_result(cx, ret);

fail:

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_cipher_pkey(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
    int encrypt)
{
    int                     rc;
    u_char                  *dst;
    size_t                  outlen;
    JSValue                 ret;
    const EVP_MD            *md;
    EVP_PKEY_CTX            *ctx;
    EVP_PKEY_cipher_t       cipher;
    EVP_PKEY_cipher_init_t  init;

    ctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL);
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed");
        return JS_EXCEPTION;
    }

    if (encrypt) {
        init = EVP_PKEY_encrypt_init;
        cipher = EVP_PKEY_encrypt;

    } else {
        init = EVP_PKEY_decrypt_init;
        cipher = EVP_PKEY_decrypt;
    }

    rc = init(ctx);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt_init() failed",
                            encrypt ? "en" : "de");
        ret = JS_EXCEPTION;
        goto fail;
    }

    md = qjs_algorithm_hash_digest(key->hash);

    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    EVP_PKEY_CTX_set_signature_md(ctx, md);
    EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md);

    rc = cipher(ctx, NULL, &outlen, data->start, data->length);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed",
                            encrypt ? "en" : "de");
        ret = JS_EXCEPTION;
        goto fail;
    }

    dst = js_malloc(cx, outlen);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        ret = JS_EXCEPTION;
        goto fail;
    }

    rc = cipher(ctx, dst, &outlen, data->start, data->length);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed",
                            encrypt ? "en" : "de");
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    ret = qjs_new_array_buffer(cx, dst, outlen);

fail:

    EVP_PKEY_CTX_free(ctx);

    return ret;
}


static JSValue
qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
    JSValue options, int encrypt)
{
    int               len, outlen, dstlen;
    u_char            *dst, *p;
    JSValue           ret, value;
    int64_t           taglen;
    njs_str_t         iv, aad;
    EVP_CIPHER_CTX    *ctx;
    const EVP_CIPHER  *cipher;

    switch (key->u.s.raw.length) {
    case 16:
        cipher = EVP_aes_128_gcm();
        break;

    case 24:
        cipher = EVP_aes_192_gcm();
        break;

    case 32:
        cipher = EVP_aes_256_gcm();
        break;

    default:
        JS_ThrowTypeError(cx, "AES-GCM invalid key length");
        return JS_EXCEPTION;
    }

    value = JS_GetPropertyStr(cx, options, "iv");
    if (JS_IsException(value)) {
        return JS_EXCEPTION;
    }

    if (JS_IsUndefined(value)) {
        JS_ThrowTypeError(cx, "AES-GCM algorithm.iv is not provided");
        return JS_EXCEPTION;
    }

    ret = qjs_typed_array_data(cx, value, &iv);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    JS_FreeValue(cx, value);

    taglen = 128;

    value = JS_GetPropertyStr(cx, options, "tagLength");
    if (JS_IsException(value)) {
        return JS_EXCEPTION;
    }

    if (!JS_IsUndefined(value)) {
        if (JS_ToInt64(cx, &taglen, value) < 0) {
            return JS_EXCEPTION;
        }
    }

    if (taglen != 32 && taglen != 64 && taglen != 96 && taglen != 104
        && taglen != 112 && taglen != 120 && taglen != 128)
    {
        JS_ThrowTypeError(cx, "AES-GCM invalid tagLength");
        return JS_EXCEPTION;
    }

    taglen /= 8;

    if (!encrypt && data->length < (size_t) taglen) {
        JS_ThrowTypeError(cx, "AES-GCM data is too short");
        return JS_EXCEPTION;
    }

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
        return JS_EXCEPTION;
    }

    if (EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt) <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        goto fail;
    }

    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.length, NULL) <= 0) {
        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
        goto fail;
    }

    if (EVP_CipherInit_ex(ctx, NULL, NULL, key->u.s.raw.start, iv.start,
                          encrypt) <= 0)
    {
        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        goto fail;
    }

    if (!encrypt) {
        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen,
                                &data->start[data->length - taglen]) <= 0)
        {
            qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
            goto fail;
        }
    }

    aad.length = 0;

    value = JS_GetPropertyStr(cx, options, "additionalData");
    if (JS_IsException(value)) {
        goto fail;
    }

    if (!JS_IsUndefined(value)) {
        ret = qjs_typed_array_data(cx, value, &aad);
        if (JS_IsException(ret)) {
            goto fail;
        }
    }

    JS_FreeValue(cx, value);

    if (aad.length != 0) {
        if (EVP_CipherUpdate(ctx, NULL, &outlen, aad.start, aad.length) <= 0) {
            qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
                                encrypt ? "Encrypt" : "Decrypt");
            goto fail;
        }
    }

    dstlen = data->length + EVP_CIPHER_CTX_block_size(ctx) + taglen;
    dst = js_malloc(cx, dstlen);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        goto fail;
    }

    if (EVP_CipherUpdate(ctx, dst, &outlen, data->start,
                         data->length - (encrypt ? 0 : taglen)) <= 0)
    {
        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        js_free(cx, dst);
        goto fail;
    }

    p = &dst[outlen];
    len = EVP_CIPHER_CTX_block_size(ctx);

    if (EVP_CipherFinal_ex(ctx, p, &len) <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        js_free(cx, dst);
        goto fail;
    }

    outlen += len;
    p += len;

    if (encrypt) {
        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, p) <= 0) {
            qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed");
            js_free(cx, dst);
            goto fail;
        }

        outlen += taglen;
    }

    ret = qjs_new_array_buffer(cx, dst, outlen);

    EVP_CIPHER_CTX_free(ctx);

    return ret;

fail:

    EVP_CIPHER_CTX_free(ctx);

    return JS_EXCEPTION;
}


static int
qjs_cipher_aes_ctr128(JSContext *cx, const EVP_CIPHER *cipher, u_char *key,
    u_char *data, size_t dlen, u_char *counter, u_char *dst, int *olen,
    int encrypt)
{
    int             len, outlen;
    int             ret;
    EVP_CIPHER_CTX  *ctx;

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
        return -1;
    }

    ret = EVP_CipherInit_ex(ctx, cipher, NULL, key, counter, encrypt);
    if (ret <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        ret = -1;
        goto fail;
    }

    ret = EVP_CipherUpdate(ctx, dst, &outlen, data, dlen);
    if (ret <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        ret = -1;
        goto fail;
    }

    ret = EVP_CipherFinal_ex(ctx, &dst[outlen], &len);
    if (ret <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        ret = -1;
        goto fail;
    }

    outlen += len;
    *olen = outlen;

    ret = 0;

fail:

    EVP_CIPHER_CTX_free(ctx);

    return ret;
}


static BIGNUM *
qjs_bn_counter128(njs_str_t *ctr, unsigned bits)
{
    unsigned  remainder, bytes;
    uint8_t   buf[16];

    remainder = bits % 8;

    if (remainder == 0) {
        bytes = bits / 8;

        return BN_bin2bn(&ctr->start[ctr->length - bytes], bytes, NULL);
    }

    bytes = (bits + 7) / 8;

    memcpy(buf, &ctr->start[ctr->length - bytes], bytes);

    buf[0] &= ~(0xFF << remainder);

    return BN_bin2bn(buf, bytes, NULL);
}


njs_inline unsigned
qjs_ceiling_div(unsigned dend, unsigned dsor)
{
    return (dsor == 0) ? 0 : 1 + (dend - 1) / dsor;
}


static void
qjs_counter128_reset(u_char *src, u_char *dst, unsigned bits)
{
    size_t    index;
    unsigned  remainder, bytes;

    bytes = bits / 8;
    remainder = bits % 8;

    memcpy(dst, src, 16);

    index = 16 - bytes;

    memset(&dst[index], 0, bytes);

    if (remainder) {
        dst[index - 1] &= 0xff << remainder;
    }
}


static JSValue
qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
    JSValue options, int encrypt)
{
    int               len, len2;
    u_char            *dst;
    BIGNUM            *total, *blocks, *left, *ctr;
    size_t            size1;
    JSValue           ret, value;
    int64_t           length;
    njs_str_t         iv;
    const EVP_CIPHER  *cipher;
    u_char            iv2[16];

    switch (key->u.s.raw.length) {
    case 16:
        cipher = EVP_aes_128_ctr();
        break;

    case 24:
        cipher = EVP_aes_192_ctr();
        break;

    case 32:
        cipher = EVP_aes_256_ctr();
        break;

    default:
        JS_ThrowTypeError(cx, "AES-CTR invalid key length");
        return JS_EXCEPTION;
    }

    value = JS_GetPropertyStr(cx, options, "counter");
    if (JS_IsException(value)) {
        return JS_EXCEPTION;
    }

    if (JS_IsUndefined(value)) {
        JS_ThrowTypeError(cx, "AES-CTR algorithm.counter is not provided");
        return JS_EXCEPTION;
    }

    ret = qjs_typed_array_data(cx, value, &iv);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    JS_FreeValue(cx, value);

    if (iv.length != 16) {
        JS_ThrowTypeError(cx, "AES-CTR algorithm.counter must be 16 bytes "
                          "long");
        return JS_EXCEPTION;
    }

    value = JS_GetPropertyStr(cx, options, "length");
    if (JS_IsException(value)) {
        return JS_EXCEPTION;
    }

    if (JS_IsUndefined(value)) {
        JS_ThrowTypeError(cx, "AES-CTR algorithm.length is not provided");
        return JS_EXCEPTION;
    }

    if (JS_ToInt64(cx, &length, value) < 0) {
        return JS_EXCEPTION;
    }

    if (length == 0 || length > 128) {
        JS_ThrowTypeError(cx, "AES-CTR algorithm.length must be between "
                          "1 and 128");
        return JS_EXCEPTION;
    }

    ctr = NULL;
    blocks = NULL;
    left = NULL;

    total = BN_new();
    if (total == NULL) {
        qjs_webcrypto_error(cx, "BN_new() failed");
        return JS_EXCEPTION;
    }

    if (BN_lshift(total, BN_value_one(), length) != 1) {
        qjs_webcrypto_error(cx, "BN_lshift() failed");
        ret = JS_EXCEPTION;
        goto fail;
    }

    ctr = qjs_bn_counter128(&iv, length);
    if (ctr == NULL) {
        qjs_webcrypto_error(cx, "BN_bin2bn() failed");
        ret = JS_EXCEPTION;
        goto fail;
    }

    blocks = BN_new();
    if (blocks == NULL) {
        qjs_webcrypto_error(cx, "BN_new() failed");
        return JS_EXCEPTION;
    }

    if (BN_set_word(blocks, qjs_ceiling_div(data->length, AES_BLOCK_SIZE))
        != 1)
    {
        qjs_webcrypto_error(cx, "BN_set_word() failed");
        ret = JS_EXCEPTION;
        goto fail;
    }

    if (BN_cmp(blocks, total) > 0) {
        JS_ThrowTypeError(cx, "AES-CTR repeated counter");
        ret = JS_EXCEPTION;
        goto fail;
    }

    left = BN_new();
    if (left == NULL) {
        qjs_webcrypto_error(cx, "BN_new() failed");
        return JS_EXCEPTION;
    }

    if (BN_sub(left, total, ctr) != 1) {
        qjs_webcrypto_error(cx, "BN_sub() failed");
        ret = JS_EXCEPTION;
        goto fail;
    }

    dst = js_malloc(cx, data->length + EVP_MAX_BLOCK_LENGTH);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        ret = JS_EXCEPTION;
        goto fail;
    }

    if (BN_cmp(left, blocks) >= 0) {
        /*
         * Doing a single run if a counter is not wrapped-around
         * during the ciphering.
         */
        if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start,
                                  data->start, data->length, iv.start, dst,
                                  &len, encrypt) < 0)
        {
            js_free(cx, dst);
            ret = JS_EXCEPTION;
            goto fail;
        }

        goto done;
    }

    /*
     * Otherwise splitting ciphering into two parts:
     *  Until the wrapping moment
     *  After the resetting counter to zero.
     */

    size1 = BN_get_word(left) * AES_BLOCK_SIZE;

    if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start, data->start,
                              size1, iv.start, dst, &len, encrypt) < 0)
    {
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    qjs_counter128_reset(iv.start, iv2, length);

    if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start,
                              &data->start[size1], data->length - size1,
                              iv2, &dst[size1], &len2, encrypt) < 0)
    {
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    len += len2;

done:

    ret = qjs_new_array_buffer(cx, dst, len);

fail:

    BN_free(total);

    if (ctr != NULL) {
        BN_free(ctr);
    }

    if (blocks != NULL) {
        BN_free(blocks);
    }

    if (left != NULL) {
        BN_free(left);
    }

    return ret;
}


static JSValue
qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
    JSValue options, int encrypt)
{
    int               rc, olen_max, olen, olen2;
    u_char            *dst;
    JSValue           ret, value;
    unsigned          remainder;
    njs_str_t         iv;
    EVP_CIPHER_CTX    *ctx;
    const EVP_CIPHER  *cipher;

    switch (key->u.s.raw.length) {
    case 16:
        cipher = EVP_aes_128_cbc();
        break;

    case 24:
        cipher = EVP_aes_192_cbc();
        break;

    case 32:
        cipher = EVP_aes_256_cbc();
        break;

    default:
        JS_ThrowTypeError(cx, "AES-CBC invalid key length");
        return JS_EXCEPTION;
    }

    value = JS_GetPropertyStr(cx, options, "iv");
    if (JS_IsException(value)) {
        return JS_EXCEPTION;
    }

    if (JS_IsUndefined(value)) {
        JS_ThrowTypeError(cx, "AES-CBC algorithm.iv is not provided");
        return JS_EXCEPTION;
    }

    ret = qjs_typed_array_data(cx, value, &iv);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    JS_FreeValue(cx, value);

    if (iv.length != 16) {
        JS_ThrowTypeError(cx, "AES-CBC algorithm.iv must be 16 bytes long");
        return JS_EXCEPTION;
    }

    olen_max = data->length + AES_BLOCK_SIZE - 1;
    remainder = olen_max % AES_BLOCK_SIZE;

    if (remainder != 0) {
        olen_max += AES_BLOCK_SIZE - remainder;
    }

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
        return JS_EXCEPTION;
    }

    rc = EVP_CipherInit_ex(ctx, cipher, NULL, key->u.s.raw.start, iv.start,
                            encrypt);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        ret = JS_EXCEPTION;
        goto fail;
    }

    dst = js_malloc(cx, olen_max);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        ret = JS_EXCEPTION;
        goto fail;
    }

    rc = EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    rc = EVP_CipherFinal_ex(ctx, &dst[olen], &olen2);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    olen += olen2;

    ret = qjs_new_array_buffer(cx, dst, olen);

fail:

    EVP_CIPHER_CTX_free(ctx);

    return ret;
}


#if (NJS_HAVE_AES_WRAP)
static JSValue
qjs_cipher_aes_kw(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key,
    int encrypt)
{
    int               olen, dstlen;
    u_char            *dst;
    JSValue           ret;
    EVP_CIPHER_CTX    *ctx;
    const EVP_CIPHER  *cipher;

    if (encrypt) {
        if (data->length < 16) {
            JS_ThrowTypeError(cx, "AES-KW data must be at least 16 bytes");
            return JS_EXCEPTION;
        }

        if (data->length % 8 != 0) {
            JS_ThrowTypeError(cx, "AES-KW data must be a multiple of 8 bytes");
            return JS_EXCEPTION;
        }

    } else {
        if (data->length < 24) {
            JS_ThrowTypeError(cx, "AES-KW data must be at least 24 bytes");
            return JS_EXCEPTION;
        }

        if (data->length % 8 != 0) {
            JS_ThrowTypeError(cx, "AES-KW data must be a multiple of 8 bytes");
            return JS_EXCEPTION;
        }
    }

    switch (key->u.s.raw.length) {
    case 16:
        cipher = EVP_aes_128_wrap();
        break;

    case 24:
        cipher = EVP_aes_192_wrap();
        break;

    case 32:
        cipher = EVP_aes_256_wrap();
        break;

    default:
        JS_ThrowTypeError(cx, "AES-KW Invalid key length");
        return JS_EXCEPTION;
    }

    ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed");
        return JS_EXCEPTION;
    }

    EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);

    if (EVP_CipherInit_ex(ctx, cipher, NULL, key->u.s.raw.start, NULL,
                          encrypt) <= 0)
    {
        qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        ret = JS_EXCEPTION;
        goto fail;
    }

    dstlen = data->length + (encrypt ? 8 : 0);
    dst = js_malloc(cx, dstlen);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        ret = JS_EXCEPTION;
        goto fail;
    }

    if (EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length) <= 0) {
        qjs_webcrypto_error(cx, "EVP_%sUpdate() failed",
                            encrypt ? "Encrypt" : "Decrypt");
        js_free(cx, dst);
        ret = JS_EXCEPTION;
        goto fail;
    }

    ret = qjs_new_array_buffer(cx, dst, olen);

fail:

    EVP_CIPHER_CTX_free(ctx);

    return ret;
}
#endif


static JSValue
qjs_export_base64url_bignum(JSContext *cx, const BIGNUM *v, size_t size)
{
    njs_str_t  src;
    u_char     buf[512];

    if (size == 0) {
        size = BN_num_bytes(v);
    }

    if (njs_bn_bn2binpad(v, &buf[0], size) <= 0) {
        JS_ThrowInternalError(cx, "njs_bn_bn2binpad() failed");
        return JS_EXCEPTION;
    }

    src.start = buf;
    src.length = size;

    return qjs_string_base64url(cx, &src);
}


static int
qjs_base64url_bignum_set(JSContext *cx, JSValue jwk, const char *key,
    const BIGNUM *v, size_t size)
{
    JSValue  value;

    value = qjs_export_base64url_bignum(cx, v, size);
    if (JS_IsException(value)) {
        return -1;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, key, value, JS_PROP_C_W_E) < 0) {
        return -1;
    }

    return 0;
}


static JSValue
qjs_export_jwk_rsa(JSContext *cx, qjs_webcrypto_key_t *key)
{
    JSValue       jwk, alg;
    njs_str_t     *nm;
    const RSA     *rsa;
    const BIGNUM  *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn, *qi_bn;

    rsa = njs_pkey_get_rsa_key(key->u.a.pkey);

    njs_rsa_get0_key(rsa, &n_bn, &e_bn, &d_bn);

    jwk = JS_NewObject(cx);
    if (JS_IsException(jwk)) {
        return JS_EXCEPTION;
    }

    if (qjs_base64url_bignum_set(cx, jwk, "n", n_bn, 0) < 0) {
        goto fail;
    }

    if (qjs_base64url_bignum_set(cx, jwk, "e", e_bn, 0) < 0) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "RSA"),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    if (key->u.a.privat) {
        njs_rsa_get0_factors(rsa, &p_bn, &q_bn);
        njs_rsa_get0_ctr_params(rsa, &dp_bn, &dq_bn, &qi_bn);

        if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, 0) < 0) {
            goto fail;
        }

        if (qjs_base64url_bignum_set(cx, jwk, "p", p_bn, 0) < 0) {
            goto fail;
        }

        if (qjs_base64url_bignum_set(cx, jwk, "q", q_bn, 0) < 0) {
            goto fail;
        }

        if (qjs_base64url_bignum_set(cx, jwk, "dp", dp_bn, 0) < 0) {
            goto fail;
        }

        if (qjs_base64url_bignum_set(cx, jwk, "dq", dq_bn, 0) < 0) {
            goto fail;
        }

        if (qjs_base64url_bignum_set(cx, jwk, "qi", qi_bn, 0) < 0) {
            goto fail;
        }
    }

    nm = &qjs_webcrypto_alg_name[key->alg->type][key->hash];

    alg = JS_NewStringLen(cx, (char *) nm->start, nm->length);
    if (JS_IsException(alg)) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "alg", alg, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    return jwk;

fail:

    JS_FreeValue(cx, jwk);

    return JS_EXCEPTION;
}


static JSValue
qjs_export_jwk_ec(JSContext *cx, qjs_webcrypto_key_t *key)
{
    int             nid, group_bits, group_bytes;
    BIGNUM          *x_bn, *y_bn;
    JSValue         jwk, name;
    njs_str_t       *cname;
    const EC_KEY    *ec;
    const BIGNUM    *d_bn;
    const EC_POINT  *pub;
    const EC_GROUP  *group;

    x_bn = NULL;
    y_bn = NULL;
    d_bn = NULL;
    jwk = JS_UNDEFINED;

    ec = njs_pkey_get_ec_key(key->u.a.pkey);

    pub = EC_KEY_get0_public_key(ec);
    group = EC_KEY_get0_group(ec);

    group_bits = EC_GROUP_get_degree(group);
    group_bytes = (group_bits / 8) + (7 + (group_bits % 8)) / 8;

    x_bn = BN_new();
    if (x_bn == NULL) {
        goto fail;
    }

    y_bn = BN_new();
    if (y_bn == NULL) {
        goto fail;
    }

    if (!njs_ec_point_get_affine_coordinates(group, pub, x_bn, y_bn)) {
        qjs_webcrypto_error(cx, "EC_POINT_get_affine_coordinates() failed");
        goto fail;
    }

    jwk = JS_NewObject(cx);
    if (JS_IsException(jwk)) {
        goto fail;
    }

    if (qjs_base64url_bignum_set(cx, jwk, "x", x_bn, group_bytes) < 0) {
        goto fail;
    }

    BN_free(x_bn);
    x_bn = NULL;

    if (qjs_base64url_bignum_set(cx, jwk, "y", y_bn, group_bytes) < 0) {
        goto fail;
    }

    BN_free(y_bn);
    y_bn = NULL;

    nid = EC_GROUP_get_curve_name(group);

    cname = qjs_algorithm_curve_name(nid);
    if (cname->length == 0) {
        JS_ThrowTypeError(cx, "Unsupported JWK EC curve: %s", OBJ_nid2sn(nid));
        goto fail;
    }

    name = JS_NewStringLen(cx, (char *) cname->start, cname->length);
    if (JS_IsException(name)) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "crv", name, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "EC"),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    if (key->u.a.privat) {
        d_bn = EC_KEY_get0_private_key(ec);

        if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, group_bytes) < 0) {
            goto fail;
        }
    }

    return jwk;

fail:

    JS_FreeValue(cx, jwk);

    if (x_bn != NULL) {
        BN_free(x_bn);
    }

    if (y_bn != NULL) {
        BN_free(y_bn);
    }

    return JS_EXCEPTION;
}


static JSValue
qjs_export_jwk_asymmetric(JSContext *cx, qjs_webcrypto_key_t *key)
{
    JSValue  jwk, ops;

    njs_assert(key->u.a.pkey != NULL);

    switch (EVP_PKEY_id(key->u.a.pkey)) {
    case EVP_PKEY_RSA:
#if (OPENSSL_VERSION_NUMBER >= 0x10101001L)
    case EVP_PKEY_RSA_PSS:
#endif
        jwk = qjs_export_jwk_rsa(cx, key);
        if (JS_IsException(jwk)) {
            return JS_EXCEPTION;
        }

        break;

    case EVP_PKEY_EC:
        jwk = qjs_export_jwk_ec(cx, key);
        if (JS_IsException(jwk)) {
            return JS_EXCEPTION;
        }

        break;

    default:
        JS_ThrowTypeError(cx, "provided key cannot be exported as JWK");
        return JS_EXCEPTION;
    }

    ops = qjs_key_ops(cx, key->usage);
    if (JS_IsException(ops)) {
        JS_FreeValue(cx, jwk);
        return JS_EXCEPTION;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", ops, JS_PROP_C_W_E) < 0) {
        JS_FreeValue(cx, jwk);
        return JS_EXCEPTION;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
                                  JS_NewBool(cx, key->extractable),
                                  JS_PROP_C_W_E) < 0)
    {
        JS_FreeValue(cx, jwk);
        return JS_EXCEPTION;
    }

    return jwk;
}


static JSValue
qjs_export_jwk_oct(JSContext *cx, qjs_webcrypto_key_t *key)
{
    JSValue              val, jwk;
    njs_str_t            *nm;
    qjs_webcrypto_alg_t  type;

    njs_assert(key->u.s.raw.start != NULL);

    jwk = JS_NewObject(cx);
    if (JS_IsException(jwk)) {
        return JS_EXCEPTION;
    }

    val = qjs_string_base64url(cx, &key->u.s.raw);
    if (JS_IsException(val)) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "k", val, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    type = key->alg->type;

    if (key->alg->type == QJS_ALGORITHM_HMAC) {
        nm = &qjs_webcrypto_alg_name[type][key->hash];
        val = JS_NewStringLen(cx, (char *) nm->start, nm->length);
        if (JS_IsException(val)) {
            goto fail;
        }

    } else {
        switch (key->u.s.raw.length) {
        case 16:
        case 24:
        case 32:
            nm = &qjs_webcrypto_alg_aes_name
                 [type - QJS_ALGORITHM_AES_GCM][(key->u.s.raw.length - 16) / 8];
            val = JS_NewStringLen(cx, (char *) nm->start, nm->length);
            if (JS_IsException(val)) {
                goto fail;
            }

            break;

        default:
            val = JS_UNDEFINED;
            break;
        }
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "alg", val, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    val = qjs_key_ops(cx, key->usage);
    if (JS_IsException(val)) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", val, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "oct"),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
                                  JS_NewBool(cx, key->extractable),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    return jwk;

fail:

    JS_FreeValue(cx, jwk);

    return JS_EXCEPTION;

}


#if (NJS_HAVE_ED25519)
static JSValue
qjs_export_jwk_okp(JSContext *cx, qjs_webcrypto_key_t *key)
{
    size_t     len;
    JSValue    jwk, ops;
    njs_str_t  raw;
    u_char     buf[64];

    njs_assert(key->u.a.pkey != NULL);

    len = 32;
    if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, buf, &len) != 1) {
        qjs_webcrypto_error(cx,
                            "EVP_PKEY_get_raw_public_key() failed");
        return JS_EXCEPTION;
    }

    jwk = JS_NewObject(cx);
    if (JS_IsException(jwk)) {
        return JS_EXCEPTION;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "OKP"),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "crv",
                                  JS_NewString(cx,
                                    key->alg->type == QJS_ALGORITHM_X25519
                                    ? "X25519" : "Ed25519"),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    raw.start = buf;
    raw.length = len;

    if (JS_DefinePropertyValueStr(cx, jwk, "x", qjs_string_base64url(cx, &raw),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    if (key->u.a.privat) {
        len = 32;
        if (EVP_PKEY_get_raw_private_key(key->u.a.pkey, buf, &len) != 1) {
            qjs_webcrypto_error(cx, "EVP_PKEY_get_raw_private_key() failed");
            goto fail;
        }

        raw.start = buf;
        raw.length = len;

        if (JS_DefinePropertyValueStr(cx, jwk, "d",
                                      qjs_string_base64url(cx, &raw),
                                      JS_PROP_C_W_E) < 0)
        {
            goto fail;
        }
    }

    ops = qjs_key_ops(cx, key->usage);
    if (JS_IsException(ops)) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", ops, JS_PROP_C_W_E) < 0) {
        goto fail;
    }

    if (JS_DefinePropertyValueStr(cx, jwk, "ext",
                                  JS_NewBool(cx, key->extractable),
                                  JS_PROP_C_W_E) < 0)
    {
        goto fail;
    }

    return jwk;

fail:

    JS_FreeValue(cx, jwk);

    return JS_EXCEPTION;
}
#endif


static JSValue
qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key)
{
    size_t                   size;
    u_char                   *dst;
    const EC_KEY             *ec;
    const EC_GROUP           *group;
    const EC_POINT           *point;
    point_conversion_form_t  form;

    njs_assert(key->u.a.pkey != NULL);

    if (key->u.a.privat) {
        JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported "
                          "in \"raw\" format", qjs_algorithm_string(key->alg));
        return JS_EXCEPTION;
    }

    ec = njs_pkey_get_ec_key(key->u.a.pkey);

    group = EC_KEY_get0_group(ec);
    point = EC_KEY_get0_public_key(ec);
    form = POINT_CONVERSION_UNCOMPRESSED;

    size = EC_POINT_point2oct(group, point, form, NULL, 0, NULL);
    if (size == 0) {
        qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed");
        return JS_EXCEPTION;
    }

    dst = js_malloc(cx, size);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    size = EC_POINT_point2oct(group, point, form, dst, size, NULL);
    if (size == 0) {
        js_free(cx, dst);
        qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed");
        return JS_EXCEPTION;
    }

    return qjs_new_array_buffer(cx, dst, size);
}


static JSValue
qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, int derive_key,
    qjs_webcrypto_key_t *key)
{
    u_char                     *k;
    size_t                     olen;
    int64_t                    length;
    JSValue                    ret, result, value, dobject;
    unsigned                   usage;
    EVP_PKEY                   *priv_pkey, *pub_pkey;
    EVP_PKEY_CTX               *pctx;
    qjs_webcrypto_key_t        *dkey, *pkey;
    qjs_webcrypto_algorithm_t  *dalg;

    result = JS_UNDEFINED;
    dobject = argv[2];

    if (derive_key) {
        dalg = qjs_key_algorithm(cx, dobject);
        if (dalg == NULL) {
            goto fail;
        }

        value = JS_GetPropertyStr(cx, dobject, "length");
        if (JS_IsException(value)) {
            goto fail;
        }

        if (JS_IsUndefined(value)) {
            JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided");
            JS_FreeValue(cx, value);
            goto fail;
        }

    } else {
        dalg = NULL;
        value = JS_DupValue(cx, dobject);
    }

    if (JS_ToInt64(cx, &length, value) < 0) {
        JS_FreeValue(cx, value);
        goto fail;
    }

    JS_FreeValue(cx, value);

    dkey = NULL;
    length /= 8;

    if (derive_key) {
        ret = qjs_key_usage(cx, argv[4], &usage);
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (usage & ~dalg->usage) {
            JS_ThrowTypeError(cx, "unsupported key usage for \"ECDH\" key");
            goto fail;
        }

        result = qjs_webcrypto_key_make(cx, dalg, usage, 0);
        if (JS_IsException(result)) {
            JS_ThrowOutOfMemory(cx);
            goto fail;
        }

        dkey = JS_GetOpaque(result, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    }

    value = JS_GetPropertyStr(cx, argv[0], "public");
    if (JS_IsException(value)) {
        goto fail;
    }

    if (JS_IsUndefined(value)) {
        JS_ThrowTypeError(cx, "ECDH algorithm.public is not provided");
        JS_FreeValue(cx, value);
        goto fail;
    }

    pkey = JS_GetOpaque(value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    JS_FreeValue(cx, value);
    if (pkey == NULL) {
        JS_ThrowTypeError(cx, "algorithm.public is not a CryptoKey object");
        goto fail;
    }

    if (pkey->alg->type != key->alg->type) {
        JS_ThrowTypeError(cx, "algorithm.public key type mismatch");
        goto fail;
    }

    if (key->alg->type == QJS_ALGORITHM_ECDH
        && key->u.a.curve != pkey->u.a.curve)
    {
        JS_ThrowTypeError(cx, "ECDH keys must use the same curve");
        goto fail;
    }

    if (!key->u.a.privat) {
        JS_ThrowTypeError(cx, "baseKey must be a private key for ECDH");
        goto fail;
    }

    if (pkey->u.a.privat) {
        JS_ThrowTypeError(cx, "algorithm.public must be a public key");
        goto fail;
    }

    priv_pkey = key->u.a.pkey;
    pub_pkey = pkey->u.a.pkey;

    pctx = EVP_PKEY_CTX_new(priv_pkey, NULL);
    if (pctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed");
        goto fail;
    }

    if (EVP_PKEY_derive_init(pctx) != 1) {
        qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed");
        EVP_PKEY_CTX_free(pctx);
        goto fail;
    }

    if (EVP_PKEY_derive_set_peer(pctx, pub_pkey) != 1) {
        qjs_webcrypto_error(cx, "EVP_PKEY_derive_set_peer() failed");
        EVP_PKEY_CTX_free(pctx);
        goto fail;
    }

    olen = (size_t) length;
    if (EVP_PKEY_derive(pctx, NULL, &olen) != 1) {
        qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed (size query)");
        EVP_PKEY_CTX_free(pctx);
        goto fail;
    }

    if (olen < (size_t) length) {
        JS_ThrowTypeError(cx, "derived bit length is too small");
        EVP_PKEY_CTX_free(pctx);
        goto fail;
    }

    k = js_malloc(cx, olen);
    if (k == NULL) {
        JS_ThrowOutOfMemory(cx);
        EVP_PKEY_CTX_free(pctx);
        goto fail;
    }

    if (EVP_PKEY_derive(pctx, k, &olen) != 1) {
        qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed");
        EVP_PKEY_CTX_free(pctx);
        js_free(cx, k);
        goto fail;
    }

    EVP_PKEY_CTX_free(pctx);

    if (derive_key) {
        if (dalg->type == QJS_ALGORITHM_HMAC) {
            ret = qjs_algorithm_hash(cx, dobject, &dkey->hash);
            if (JS_IsException(ret)) {
                js_free(cx, k);
                goto fail;
            }
        }

        dkey->extractable = JS_ToBool(cx, argv[3]);

        dkey->u.s.raw.start = k;
        dkey->u.s.raw.length = length;

    } else {
        result = qjs_new_array_buffer(cx, k, length);
    }

    return qjs_promise_result(cx, result);

fail:
    JS_FreeValue(cx, result);

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv, int derive_key)
{
    int                        rc;
    u_char                     *k;
    size_t                     olen;
    int64_t                    iterations, length;
    JSValue                    ret, val, aobject, dobject, dkey_value;
    unsigned                   usage, mask;
    njs_str_t                  salt, info;
    const EVP_MD               *md;
    EVP_PKEY_CTX               *pctx;
    qjs_webcrypto_key_t        *key, *dkey;
    qjs_webcrypto_hash_t       hash;
    qjs_webcrypto_algorithm_t  *alg, *dalg;

    aobject = argv[0];

    alg = qjs_key_algorithm(cx, aobject);
    if (alg == NULL) {
        return JS_EXCEPTION;
    }

    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"baseKey\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    mask = derive_key ? QJS_KEY_USAGE_DERIVE_KEY : QJS_KEY_USAGE_DERIVE_BITS;
    if (!(key->usage & mask)) {
        JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation",
                          derive_key ? "deriveKey" : "deriveBits");
        return JS_EXCEPTION;
    }

    if (key->alg != alg) {
        JS_ThrowTypeError(cx, "cannot derive %s using \"%s\" with \"%s\" key",
                          derive_key ? "key" : "bits",
                          qjs_algorithm_string(key->alg),
                          qjs_algorithm_string(alg));
        return JS_EXCEPTION;
    }

    if (alg->type == QJS_ALGORITHM_ECDH
        || alg->type == QJS_ALGORITHM_X25519)
    {
        return qjs_derive_ecdh(cx, argv, argc, derive_key, key);
    }

    dobject = argv[2];

    if (derive_key) {
        dalg = qjs_key_algorithm(cx, dobject);
        if (dalg == NULL) {
            return JS_EXCEPTION;
        }

        ret = JS_GetPropertyStr(cx, dobject, "length");
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

        if (JS_IsUndefined(ret)) {
            JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided");
            return JS_EXCEPTION;
        }

    } else {
        dalg = NULL;
        ret = JS_DupValue(cx, dobject);
    }

    if (JS_ToInt64(cx, &length, ret) < 0) {
        return JS_EXCEPTION;
    }

    JS_FreeValue(cx, ret);

    dkey = NULL;
    length /= 8;
    dkey_value = JS_UNDEFINED;

    if (derive_key) {
        switch (dalg->type) {
        case QJS_ALGORITHM_AES_GCM:
        case QJS_ALGORITHM_AES_CTR:
        case QJS_ALGORITHM_AES_CBC:
        case QJS_ALGORITHM_AES_KW:

            if (length != 16 && length != 24 && length != 32) {
                JS_ThrowTypeError(cx, "deriveKey \"%s\" length must be "
                                  "128, 192, or 256",
                                  qjs_algorithm_string(dalg));
                return JS_EXCEPTION;
            }

            break;

        case QJS_ALGORITHM_HMAC:
            break;

        default:
            JS_ThrowTypeError(cx, "not implemented deriveKey: \"%s\"",
                              qjs_algorithm_string(dalg));
            return JS_EXCEPTION;
        }

        ret = qjs_key_usage(cx, argv[4], &usage);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

        if (usage & ~dalg->usage) {
            JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
                              qjs_algorithm_string(alg));
            return JS_EXCEPTION;
        }

        dkey_value = qjs_webcrypto_key_make(cx, dalg, usage, 0);
        if (JS_IsException(dkey_value)) {
            JS_ThrowOutOfMemory(cx);
            return JS_EXCEPTION;
        }

        dkey = JS_GetOpaque(dkey_value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    }

    k = js_malloc(cx, length);
    if (k == NULL) {
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    switch (alg->type) {
    case QJS_ALGORITHM_PBKDF2:
        ret = qjs_algorithm_hash(cx, aobject, &hash);
        if (JS_IsException(ret)) {
            goto fail;
        }

        val = JS_GetPropertyStr(cx, aobject, "salt");
        if (JS_IsException(val)) {
            goto fail;
        }

        ret = qjs_typed_array_data(cx, val, &salt);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (salt.length < 16) {
            JS_ThrowTypeError(cx, "PBKDF2 algorithm.salt must be at least "
                              "16 bytes long");
            goto fail;
        }

        ret = JS_GetPropertyStr(cx, aobject, "iterations");
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (JS_IsUndefined(ret)) {
            JS_ThrowTypeError(cx, "PBKDF2 algorithm.iterations is not provided");
            goto fail;
        }

        if (JS_ToInt64(cx, &iterations, ret) < 0) {
            goto fail;
        }

        JS_FreeValue(cx, ret);

        md = qjs_algorithm_hash_digest(hash);

        rc = PKCS5_PBKDF2_HMAC((char *) key->u.s.raw.start, key->u.s.raw.length,
                               salt.start, salt.length, iterations, md, length,
                               k);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "PKCS5_PBKDF2_HMAC() failed");
            goto fail;
        }

        break;

    case QJS_ALGORITHM_HKDF:
#ifdef EVP_PKEY_HKDF
        ret = qjs_algorithm_hash(cx, aobject, &hash);
        if (JS_IsException(ret)) {
            goto fail;
        }

        val = JS_GetPropertyStr(cx, aobject, "salt");
        if (JS_IsException(val)) {
            goto fail;
        }

        ret = qjs_typed_array_data(cx, val, &salt);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            goto fail;
        }

        val = JS_GetPropertyStr(cx, aobject, "info");
        if (JS_IsException(val)) {
            goto fail;
        }

        ret = qjs_typed_array_data(cx, val, &info);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            goto fail;
        }

        pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
        if (pctx == NULL) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
            goto fail;
        }

        rc = EVP_PKEY_derive_init(pctx);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed");
            goto free;
        }

        md = qjs_algorithm_hash_digest(hash);

        rc = EVP_PKEY_CTX_set_hkdf_md(pctx, md);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_hkdf_md() failed");
            goto free;
        }

        rc = EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt.start, salt.length);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_salt() failed");
            goto free;
        }

        rc = EVP_PKEY_CTX_set1_hkdf_key(pctx, key->u.s.raw.start,
                                        key->u.s.raw.length);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_key() failed");
            goto free;
        }

        rc = EVP_PKEY_CTX_add1_hkdf_info(pctx, info.start, info.length);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_add1_hkdf_info() failed");
            goto free;
        }

        olen = (size_t) length;
        rc = EVP_PKEY_derive(pctx, k, &olen);
        if (rc <= 0 || olen != (size_t) length) {
            qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed");
            goto free;
        }

free:
        EVP_PKEY_CTX_free(pctx);

        if (rc <= 0) {
            goto fail;
        }

        break;
#else
        (void) pctx;
        (void) olen;
        (void) &string_info;
        (void) &info;
#endif

    default:
        JS_ThrowTypeError(cx, "not implemented deriveKey algorithm: \"%s\"",
                          qjs_algorithm_string(alg));
        goto fail;
    }

    if (derive_key) {
        if (dalg->type == QJS_ALGORITHM_HMAC) {
            ret = qjs_algorithm_hash(cx, dobject, &dkey->hash);
            if (JS_IsException(ret)) {
                goto fail;
            }
        }

        dkey->extractable = JS_ToBool(cx, argv[3]);
        dkey->u.s.raw.start = k;
        dkey->u.s.raw.length = length;

        ret = dkey_value;

    } else {

        ret = qjs_new_array_buffer(cx, k, length);
    }

    return qjs_promise_result(cx, ret);

fail:

    JS_FreeValue(cx, dkey_value);

    js_free(cx, k);

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue               ret;
    u_char                *dst;
    unsigned              olen;
    njs_str_t             data;
    const EVP_MD          *md;
    qjs_webcrypto_hash_t  hash;

    ret = qjs_algorithm_hash(cx, argv[0], &hash);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    ret = qjs_typed_array_data(cx, argv[1], &data);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    md = qjs_algorithm_hash_digest(hash);
    olen = EVP_MD_size(md);

    dst = js_malloc(cx, olen);
    if (dst == NULL) {
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    if (EVP_Digest(data.start, data.length, dst, &olen, md, NULL) <= 0) {
        js_free(cx, dst);
        qjs_webcrypto_error(cx, "EVP_Digest() failed");
        return JS_EXCEPTION;
    }

    ret = qjs_new_array_buffer(cx, dst, olen);

    return ret;
}


static JSValue
qjs_webcrypto_export_key_raw(JSContext *cx, qjs_webcrypto_key_t *key,
    qjs_webcrypto_key_format_t fmt)
{
    BIO                  *bio;
    BUF_MEM              *mem;
    JSValue              ret;
    PKCS8_PRIV_KEY_INFO  *pkcs8;

    switch (fmt) {
    case QJS_KEY_FORMAT_JWK:
        switch (key->alg->type) {
        case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
        case QJS_ALGORITHM_RSA_PSS:
        case QJS_ALGORITHM_RSA_OAEP:
        case QJS_ALGORITHM_ECDSA:
        case QJS_ALGORITHM_ECDH:
            return qjs_export_jwk_asymmetric(cx, key);

#if (NJS_HAVE_ED25519)
        case QJS_ALGORITHM_ED25519:
        case QJS_ALGORITHM_X25519:
            return qjs_export_jwk_okp(cx, key);
#endif

        case QJS_ALGORITHM_AES_GCM:
        case QJS_ALGORITHM_AES_CTR:
        case QJS_ALGORITHM_AES_CBC:
        case QJS_ALGORITHM_AES_KW:
        case QJS_ALGORITHM_HMAC:
            return qjs_export_jwk_oct(cx, key);

        default:
            JS_ThrowTypeError(cx, "provided key of \"%s\" cannot be exported "
                              "as JWK", qjs_algorithm_string(key->alg));
            return JS_EXCEPTION;
        }

    case QJS_KEY_FORMAT_PKCS8:
        if (!key->u.a.privat) {
            JS_ThrowTypeError(cx, "public key of \"%s\" cannot be exported "
                              "as PKCS8", qjs_algorithm_string(key->alg));
            return JS_EXCEPTION;
        }

        bio = BIO_new(BIO_s_mem());
        if (bio == NULL) {
            qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed");
            return JS_EXCEPTION;
        }

        njs_assert(key->u.a.pkey != NULL);

        pkcs8 = EVP_PKEY2PKCS8(key->u.a.pkey);
        if (pkcs8 == NULL) {
            BIO_free(bio);
            qjs_webcrypto_error(cx, "EVP_PKEY2PKCS8() failed");
            return JS_EXCEPTION;
        }

        if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio, pkcs8)) {
            BIO_free(bio);
            PKCS8_PRIV_KEY_INFO_free(pkcs8);
            qjs_webcrypto_error(cx,
                                "i2d_PKCS8_PRIV_KEY_INFO_bio() failed");
            return JS_EXCEPTION;
        }

        BIO_get_mem_ptr(bio, &mem);

        ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data,
                                    mem->length);
        BIO_free(bio);
        PKCS8_PRIV_KEY_INFO_free(pkcs8);

        return ret;

    case QJS_KEY_FORMAT_SPKI:
        if (key->u.a.privat) {
            JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported "
                              "as SPKI", qjs_algorithm_string(key->alg));
            return JS_EXCEPTION;
        }

        bio = BIO_new(BIO_s_mem());
        if (bio == NULL) {
            qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed");
            return JS_EXCEPTION;
        }

        njs_assert(key->u.a.pkey != NULL);

        if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) {
            BIO_free(bio);
            qjs_webcrypto_error(cx, "i2d_PUBKEY_bio() failed");
            return JS_EXCEPTION;
        }

        BIO_get_mem_ptr(bio, &mem);

        ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data,
                                    mem->length);
        BIO_free(bio);

        return ret;

    case QJS_KEY_FORMAT_RAW:
    default:
        if (key->alg->type == QJS_ALGORITHM_ECDSA
            || key->alg->type == QJS_ALGORITHM_ECDH)
        {
            return qjs_export_raw_ec(cx, key);
        }

#if (NJS_HAVE_ED25519)
        if (key->alg->type == QJS_ALGORITHM_ED25519
            || key->alg->type == QJS_ALGORITHM_X25519)
        {
            size_t  raw_len;
            u_char  raw_buf[32];

            raw_len = 32;
            if (EVP_PKEY_get_raw_public_key(key->u.a.pkey, raw_buf, &raw_len)
                != 1)
            {
                qjs_webcrypto_error(cx, "EVP_PKEY_get_raw_public_key() failed");
                return JS_EXCEPTION;
            }

            return JS_NewArrayBufferCopy(cx, raw_buf, raw_len);
        }
#endif

        return JS_NewArrayBufferCopy(cx, key->u.s.raw.start,
                                     key->u.s.raw.length);
    }
}


static JSValue
qjs_webcrypto_cipher_core(JSContext *cx, njs_str_t *data,
    qjs_webcrypto_key_t *key, JSValue options,
    qjs_webcrypto_algorithm_t *alg, int encrypt)
{
    switch (alg->type) {
    case QJS_ALGORITHM_RSA_OAEP:
        return qjs_cipher_pkey(cx, data, key, encrypt);

    case QJS_ALGORITHM_AES_GCM:
        return qjs_cipher_aes_gcm(cx, data, key, options, encrypt);

    case QJS_ALGORITHM_AES_CTR:
        return qjs_cipher_aes_ctr(cx, data, key, options, encrypt);

    case QJS_ALGORITHM_AES_CBC:
        return qjs_cipher_aes_cbc(cx, data, key, options, encrypt);

#if (NJS_HAVE_AES_WRAP)
    case QJS_ALGORITHM_AES_KW:
        return qjs_cipher_aes_kw(cx, data, key, encrypt);
#endif

    default:
        JS_ThrowTypeError(cx, "not implemented");
        return JS_EXCEPTION;
    }
}


static JSValue
qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue                     ret;
    qjs_webcrypto_key_t         *key;
    qjs_webcrypto_key_format_t  fmt;

    fmt = qjs_key_format(cx, argv[0]);

    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    if (!(fmt & key->alg->fmt)) {
        JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key",
                          qjs_format_string(fmt),
                          qjs_algorithm_string(key->alg));
        return JS_EXCEPTION;
    }

    if (!key->extractable) {
        JS_ThrowTypeError(cx, "provided key cannot be extracted");
        return JS_EXCEPTION;
    }

    ret = qjs_webcrypto_export_key_raw(cx, key, fmt);
    if (JS_IsException(ret)) {
        return qjs_promise_result(cx, JS_EXCEPTION);
    }

    return qjs_promise_result(cx, ret);
}


#if (NJS_HAVE_ED25519)
static int
qjs_webcrypto_generate_25519_keypair(JSContext *cx, int pkey_id,
    qjs_webcrypto_key_t *wkey, qjs_webcrypto_algorithm_t *alg, unsigned usage,
    int extractable, unsigned priv_usage, unsigned pub_usage, JSValue *key,
    JSValue *obj_ret)
{
    JSValue              keypub, obj;
    EVP_PKEY_CTX         *ctx;
    qjs_webcrypto_key_t  *wkeypub;

    keypub = JS_UNDEFINED;

    ctx = EVP_PKEY_CTX_new_id(pkey_id, NULL);
    if (ctx == NULL) {
        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
        return -1;
    }

    if (EVP_PKEY_keygen_init(ctx) <= 0) {
        EVP_PKEY_CTX_free(ctx);
        qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
        return -1;
    }

    if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
        EVP_PKEY_CTX_free(ctx);
        qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
        return -1;
    }

    EVP_PKEY_CTX_free(ctx);

    wkey->u.a.privat = 1;
    wkey->usage = priv_usage & usage;

    keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
    if (JS_IsException(keypub)) {
        return -1;
    }

    if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
        JS_FreeValue(cx, keypub);
        qjs_webcrypto_error(cx, "njs_pkey_up_ref() failed");
        return -1;
    }

    wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    wkeypub->u.a.pkey = wkey->u.a.pkey;
    wkeypub->usage = pub_usage & usage;

    obj = JS_NewObject(cx);
    if (JS_IsException(obj)) {
        JS_FreeValue(cx, keypub);
        return -1;
    }

    if (JS_SetPropertyStr(cx, obj, "privateKey", *key) < 0) {
        *key = JS_UNDEFINED;
        JS_FreeValue(cx, keypub);
        JS_FreeValue(cx, obj);
        return -1;
    }

    *key = JS_UNDEFINED;

    if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
        JS_FreeValue(cx, obj);
        return -1;
    }

    *obj_ret = obj;

    return 0;
}
#endif


static JSValue
qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val,
    int argc, JSValueConst *argv)
{
    int                        n, extractable;
    JSValue                    ret, key, keypub, options, obj;
    unsigned                   usage;
    EVP_PKEY_CTX               *ctx;
    qjs_webcrypto_key_t        *wkey, *wkeypub;
    qjs_webcrypto_algorithm_t  *alg;

    ctx = NULL;
    options = argv[0];
    key = JS_UNDEFINED;
    keypub = JS_UNDEFINED;
    obj = JS_UNDEFINED;

    alg = qjs_key_algorithm(cx, options);
    if (alg == NULL) {
        return JS_EXCEPTION;
    }

    ret = qjs_key_usage(cx, argv[2], &usage);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    if (usage & ~alg->usage) {
        JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
                          qjs_algorithm_string(alg));
        return JS_EXCEPTION;
    }

    extractable = JS_ToBool(cx, argv[1]);
    key = qjs_webcrypto_key_make(cx, alg, usage, extractable);
    if (JS_IsException(key)) {
        return JS_EXCEPTION;
    }

    wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);

    switch (alg->type) {
    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
    case QJS_ALGORITHM_RSA_PSS:
    case QJS_ALGORITHM_RSA_OAEP:
        ret = qjs_algorithm_hash(cx, options, &wkey->hash);
        if (JS_IsException(ret)) {
            goto fail;
        }

        ret = JS_GetPropertyStr(cx, options, "modulusLength");
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (!JS_IsNumber(ret)) {
            JS_FreeValue(cx, ret);
            JS_ThrowTypeError(cx, "\"modulusLength\" is not a number");
            goto fail;
        }

        if (JS_ToInt32(cx, &n, ret) < 0) {
            goto fail;
        }

        ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
        if (ctx == NULL) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
            goto fail;
        }

        if (EVP_PKEY_keygen_init(ctx) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
            goto fail;
        }

        if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, n) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_keygen_bits() "
                                "failed");
            goto fail;
        }

        if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
            goto fail;
        }

        EVP_PKEY_CTX_free(ctx);
        ctx = NULL;

        wkey->u.a.privat = 1;
        wkey->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP)
                        ? QJS_KEY_USAGE_DECRYPT
                        : QJS_KEY_USAGE_SIGN;

        keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
        if (JS_IsException(keypub)) {
            goto fail;
        }

        if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
            qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed");
            goto fail;
        }

        wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);

        wkeypub->u.a.pkey = wkey->u.a.pkey;
        wkeypub->hash = wkey->hash;
        wkeypub->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP)
                          ? QJS_KEY_USAGE_ENCRYPT
                          : QJS_KEY_USAGE_VERIFY;

        obj = JS_NewObject(cx);
        if (JS_IsException(obj)) {
            goto fail;
        }

        if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) {
            key = JS_UNDEFINED;
            goto fail;
        }

        key = JS_UNDEFINED;

        if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
            keypub = JS_UNDEFINED;
            goto fail;
        }

        break;

    case QJS_ALGORITHM_ECDSA:
    case QJS_ALGORITHM_ECDH:
        ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
        if (JS_IsException(ret)) {
            goto fail;
        }

        ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
        if (ctx == NULL) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed");
            goto fail;
        }

        if (EVP_PKEY_keygen_init(ctx) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed");
            goto fail;
        }

        if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, wkey->u.a.curve) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_ec_paramgen_curve_nid() "
                                "failed");
            goto fail;
        }

        if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed");
            goto fail;
        }

        EVP_PKEY_CTX_free(ctx);
        ctx = NULL;

        wkey->u.a.privat = 1;

        if (alg->type == QJS_ALGORITHM_ECDSA) {
            wkey->usage = QJS_KEY_USAGE_SIGN;

        } else {
            /* ECDH */
            wkey->usage = QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS;
        }

        keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable);
        if (JS_IsException(keypub)) {
            goto fail;
        }

        if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) {
            qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed");
            goto fail;
        }

        wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);

        wkeypub->u.a.pkey = wkey->u.a.pkey;
        wkeypub->u.a.curve = wkey->u.a.curve;

        if (alg->type == QJS_ALGORITHM_ECDSA) {
            wkeypub->usage = QJS_KEY_USAGE_VERIFY;

        } else {
            /* ECDH */
            wkeypub->usage = QJS_KEY_USAGE_DERIVE_KEY
                             | QJS_KEY_USAGE_DERIVE_BITS;
        }

        obj = JS_NewObject(cx);
        if (JS_IsException(obj)) {
            goto fail;
        }

        if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) {
            key = JS_UNDEFINED;
            goto fail;
        }

        key = JS_UNDEFINED;

        if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) {
            keypub = JS_UNDEFINED;
            goto fail;
        }

        break;

#if (NJS_HAVE_ED25519)
    case QJS_ALGORITHM_ED25519:
        n = qjs_webcrypto_generate_25519_keypair(cx, EVP_PKEY_ED25519,
                                                 wkey, alg, usage,
                                                 extractable,
                                                 QJS_KEY_USAGE_SIGN,
                                                 QJS_KEY_USAGE_VERIFY,
                                                 &key, &obj);
        if (n < 0) {
            goto fail;
        }

        break;

    case QJS_ALGORITHM_X25519:
        n = qjs_webcrypto_generate_25519_keypair(cx, EVP_PKEY_X25519,
                                                 wkey, alg, usage,
                                                 extractable,
                                                 QJS_KEY_USAGE_DERIVE_KEY
                                                 | QJS_KEY_USAGE_DERIVE_BITS,
                                                 0, &key, &obj);
        if (n < 0) {
            goto fail;
        }

        break;
#endif

    case QJS_ALGORITHM_AES_GCM:
    case QJS_ALGORITHM_AES_CTR:
    case QJS_ALGORITHM_AES_CBC:
    case QJS_ALGORITHM_AES_KW:
    case QJS_ALGORITHM_HMAC:
        if (alg->type == QJS_ALGORITHM_HMAC) {
            ret = qjs_algorithm_hash(cx, options, &wkey->hash);
            if (JS_IsException(ret)) {
                goto fail;
            }

            wkey->u.s.raw.length =
                            EVP_MD_size(qjs_algorithm_hash_digest(wkey->hash));

        } else {
            ret = JS_GetPropertyStr(cx, options, "length");
            if (JS_IsException(ret)) {
                goto fail;
            }

            if (!JS_IsNumber(ret)) {
                JS_FreeValue(cx, ret);
                JS_ThrowTypeError(cx, "length is not a number");
                goto fail;
            }

            if (JS_ToInt32(cx, &n, ret) < 0) {
                goto fail;
            }

            wkey->u.s.raw.length = n / 8;

            if (wkey->u.s.raw.length != 16
                && wkey->u.s.raw.length != 24
                && wkey->u.s.raw.length != 32)
            {
                JS_ThrowTypeError(cx, "length for \"%s\" key should be "
                                  "one of 128, 192, 256",
                                  qjs_algorithm_string(alg));
                goto fail;
            }
        }

        wkey->u.s.raw.start = js_malloc(cx, wkey->u.s.raw.length);
        if (wkey->u.s.raw.start == NULL) {
            JS_ThrowOutOfMemory(cx);
            goto fail;
        }

        if (RAND_bytes(wkey->u.s.raw.start, wkey->u.s.raw.length) <= 0) {
            qjs_webcrypto_error(cx, "RAND_bytes() failed");
            goto fail;
        }

        obj = key;

        break;

    default:
        JS_ThrowTypeError(cx, "not implemented generateKey algorithm: \"%s\"",
                          qjs_algorithm_string(alg));
        goto fail;
    }

    return qjs_promise_result(cx, obj);

fail:

    if (ctx != NULL) {
        EVP_PKEY_CTX_free(ctx);
    }

    JS_FreeValue(cx, key);
    JS_FreeValue(cx, keypub);
    JS_FreeValue(cx, obj);

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static BIGNUM *
qjs_import_base64url_bignum(JSContext *cx, JSValue value)
{
    BIGNUM     *bn;
    njs_str_t  data, decoded;
    u_char     buf[512];

    data.start = (u_char *) JS_ToCStringLen(cx, &data.length, value);
    if (data.start == NULL) {
        JS_ThrowOutOfMemory(cx);
        return NULL;
    }

    decoded.length = qjs_base64url_decode_length(cx, &data);

    if (decoded.length > sizeof(buf)) {
        JS_ThrowRangeError(cx, "JWK key too long: %zu > 512", decoded.length);
        return NULL;
    }

    decoded.start = buf;

    qjs_base64url_decode(cx, &data, &decoded);

    bn = BN_bin2bn(decoded.start, decoded.length, NULL);
    JS_FreeCString(cx, (char *) data.start);

    return bn;
}


static EVP_PKEY *
qjs_import_jwk_rsa(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
{
    RSA                    *rsa;
    BIGNUM                 *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn,
                           *qi_bn;
    JSValue                ret, val, n, e, d, p, q, dp, dq, qi;
    unsigned               usage;
    EVP_PKEY               *pkey;
    njs_str_t              alg;
    qjs_webcrypto_entry_t  *w;

    e = JS_UNDEFINED;
    d = JS_UNDEFINED;

    n = JS_GetPropertyStr(cx, jwk, "n");
    if (JS_IsException(n)) {
        goto fail0;
    }

    e = JS_GetPropertyStr(cx, jwk, "e");
    if (JS_IsException(e)) {
        goto fail0;
    }

    d = JS_GetPropertyStr(cx, jwk, "d");
    if (JS_IsException(d)) {
        goto fail0;
    }

    if (!JS_IsString(n)
        || !JS_IsString(e)
        || (!JS_IsUndefined(d) && !JS_IsString(d)))
    {
fail0:
        JS_FreeValue(cx, n);
        JS_FreeValue(cx, e);
        JS_FreeValue(cx, d);
        JS_ThrowTypeError(cx, "Invalid JWK RSA key");
        return NULL;
    }

    key->u.a.privat = JS_IsString(d);

    val = JS_GetPropertyStr(cx, jwk, "key_ops");
    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
        ret = qjs_key_usage(cx, val, &usage);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            goto fail0;
        }

        if ((key->usage & usage) != key->usage) {
            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
            goto fail0;
        }
    }

    ret = JS_GetPropertyStr(cx, jwk, "alg");
    if (!JS_IsException(ret) && !JS_IsUndefined(ret)) {
        alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, ret);
        JS_FreeValue(cx, ret);
        if (alg.start == NULL) {
            JS_ThrowOutOfMemory(cx);
            goto fail0;
        }

        for (w = &qjs_webcrypto_alg_hash[0]; w->name.length != 0; w++) {
            if (njs_strstr_eq(&alg, &w->name)) {
                key->hash = w->value;
                break;
            }
        }

        JS_FreeCString(cx, (char *) alg.start);
    }

    if (key->extractable) {
        ret = JS_GetPropertyStr(cx, jwk, "ext");
        if (!JS_IsException(ret)
            && !JS_IsUndefined(ret)
            && !JS_ToBool(cx, ret))
        {
            JS_FreeValue(cx, ret);
            JS_ThrowTypeError(cx, "JWK RSA is not extractable");
            goto fail0;
        }

        JS_FreeValue(cx, ret);
    }

    rsa = RSA_new();
    if (rsa == NULL) {
        qjs_webcrypto_error(cx, "RSA_new() failed");
        goto fail0;
    }

    p = JS_UNDEFINED;
    q = JS_UNDEFINED;
    dp = JS_UNDEFINED;
    dq = JS_UNDEFINED;
    qi = JS_UNDEFINED;

    n_bn = qjs_import_base64url_bignum(cx, n);
    if (n_bn == NULL) {
        goto fail;
    }

    e_bn = qjs_import_base64url_bignum(cx, e);
    if (e_bn == NULL) {
        BN_free(n_bn);
        goto fail;
    }

    if (!njs_rsa_set0_key(rsa, n_bn, e_bn, NULL)) {
        BN_free(n_bn);
        BN_free(e_bn);
        qjs_webcrypto_error(cx, "RSA_set0_key() failed");
        goto fail;
    }

    if (!key->u.a.privat) {
        goto done;
    }

    p = JS_GetPropertyStr(cx, jwk, "p");
    if (JS_IsException(p)) {
        goto fail1;
    }

    q = JS_GetPropertyStr(cx, jwk, "q");
    if (JS_IsException(q)) {
        goto fail1;
    }

    dp = JS_GetPropertyStr(cx, jwk, "dp");
    if (JS_IsException(dp)) {
        goto fail1;
    }

    dq = JS_GetPropertyStr(cx, jwk, "dq");
    if (JS_IsException(dq)) {
        goto fail1;
    }

    qi = JS_GetPropertyStr(cx, jwk, "qi");
    if (JS_IsException(qi)) {
        goto fail1;
    }

    if (!JS_IsString(d)
        || !JS_IsString(p)
        || !JS_IsString(q)
        || !JS_IsString(dp)
        || !JS_IsString(dq)
        || !JS_IsString(qi))
    {
fail1:
        JS_ThrowTypeError(cx, "Invalid JWK RSA key");
        goto fail;
    }

    d_bn = qjs_import_base64url_bignum(cx, d);
    if (d_bn == NULL) {
        goto fail;
    }

    if (!njs_rsa_set0_key(rsa, NULL, NULL, d_bn)) {
        BN_free(d_bn);
        qjs_webcrypto_error(cx, "RSA_set0_key() failed");
        goto fail;
    }

    p_bn = qjs_import_base64url_bignum(cx, p);
    if (p_bn == NULL) {
        goto fail;
    }

    q_bn = qjs_import_base64url_bignum(cx, q);
    if (q_bn == NULL) {
        BN_free(p_bn);
        goto fail;
    }

    if (!njs_rsa_set0_factors(rsa, p_bn, q_bn)) {
        BN_free(p_bn);
        BN_free(q_bn);

        qjs_webcrypto_error(cx, "RSA_set0_factors() failed");
        goto fail;
    }

    dp_bn = qjs_import_base64url_bignum(cx, dp);
    if (dp_bn == NULL) {
        goto fail;
    }

    dq_bn = qjs_import_base64url_bignum(cx, dq);
    if (dq_bn == NULL) {
        BN_free(dp_bn);
        goto fail;
    }

    qi_bn = qjs_import_base64url_bignum(cx, qi);
    if (qi_bn == NULL) {
        BN_free(dp_bn);
        BN_free(dq_bn);
        goto fail;
    }

    if (!njs_rsa_set0_ctr_params(rsa, dp_bn, dq_bn, qi_bn)) {
        BN_free(dp_bn);
        BN_free(dq_bn);
        BN_free(qi_bn);
        qjs_webcrypto_error(cx, "RSA_set0_crt_params() failed");
        goto fail;
    }

    JS_FreeValue(cx, p);
    JS_FreeValue(cx, q);
    JS_FreeValue(cx, dp);
    JS_FreeValue(cx, dq);
    JS_FreeValue(cx, qi);

done:

    JS_FreeValue(cx, n);
    JS_FreeValue(cx, e);
    JS_FreeValue(cx, d);

    pkey = EVP_PKEY_new();
    if (pkey == NULL) {
        goto fail;
    }

    if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
        EVP_PKEY_free(pkey);
        goto fail;
    }

    RSA_free(rsa);

    return pkey;

fail:

    JS_FreeValue(cx, n);
    JS_FreeValue(cx, e);
    JS_FreeValue(cx, d);
    JS_FreeValue(cx, p);
    JS_FreeValue(cx, q);
    JS_FreeValue(cx, dp);
    JS_FreeValue(cx, dq);
    JS_FreeValue(cx, qi);

    RSA_free(rsa);

    return NULL;
}


static EVP_PKEY *
qjs_import_jwk_ec(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
{
    int                    curve;
    EC_KEY                 *ec;
    BIGNUM                 *x_bn, *y_bn, *d_bn;
    JSValue                ret, val, x, y, d;
    unsigned               usage;
    EVP_PKEY               *pkey;
    njs_str_t              name;
    qjs_webcrypto_entry_t  *e;

    x = JS_UNDEFINED;
    y = JS_UNDEFINED;
    d = JS_UNDEFINED;

    x = JS_GetPropertyStr(cx, jwk, "x");
    if (JS_IsException(x)) {
        goto fail0;
    }

    y = JS_GetPropertyStr(cx, jwk, "y");
    if (JS_IsException(y)) {
        goto fail0;
    }

    d = JS_GetPropertyStr(cx, jwk, "d");
    if (JS_IsException(d)) {
        goto fail0;
    }

    if (!JS_IsString(x)
        || !JS_IsString(y)
        || (!JS_IsUndefined(d) && !JS_IsString(d)))
    {
fail0:
        JS_FreeValue(cx, x);
        JS_FreeValue(cx, y);
        JS_FreeValue(cx, d);
        JS_ThrowTypeError(cx, "Invalid JWK EC key");
        return NULL;
    }

    key->u.a.privat = JS_IsString(d);

    val = JS_GetPropertyStr(cx, jwk, "key_ops");
    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
        ret = qjs_key_usage(cx, val, &usage);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            goto fail0;
        }

        if ((key->usage & usage) != key->usage) {
            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
            goto fail0;
        }
    }

    if (key->extractable) {
        ret = JS_GetPropertyStr(cx, jwk, "ext");
        if (!JS_IsException(ret)
            && !JS_IsUndefined(ret)
            && !JS_ToBool(cx, ret))
        {
            JS_FreeValue(cx, ret);
            JS_ThrowTypeError(cx, "JWK EC is not extractable");
            goto fail0;
        }

        JS_FreeValue(cx, ret);
    }

    curve = 0;

    ret = JS_GetPropertyStr(cx, jwk, "crv");
    if (!JS_IsException(ret) && !JS_IsUndefined(ret)) {
        name.start = (u_char *) JS_ToCStringLen(cx, &name.length, ret);
        JS_FreeValue(cx, ret);
        if (name.start == NULL) {
            JS_ThrowOutOfMemory(cx);
            goto fail0;
        }

        for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
            if (njs_strstr_eq(&name, &e->name)) {
                curve = e->value;
                break;
            }
        }

        JS_FreeCString(cx, (char *) name.start);
    }

    if (curve != key->u.a.curve) {
        JS_ThrowTypeError(cx, "JWK EC curve mismatch");
        goto fail0;
    }

    ec = EC_KEY_new_by_curve_name(key->u.a.curve);
    if (ec == NULL) {
        qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed");
        goto fail0;
    }

    y_bn = NULL;
    d_bn = NULL;

    x_bn = qjs_import_base64url_bignum(cx, x);
    if (x_bn == NULL) {
        goto fail;
    }

    y_bn = qjs_import_base64url_bignum(cx, y);
    if (y_bn == NULL) {
        goto fail;
    }

    if (!EC_KEY_set_public_key_affine_coordinates(ec, x_bn, y_bn)) {
        qjs_webcrypto_error(cx, "EC_KEY_set_public_key_affine_coordinates() "
                            "failed");
        goto fail;
    }

    BN_free(x_bn);
    x_bn = NULL;

    BN_free(y_bn);
    y_bn = NULL;

    if (key->u.a.privat) {
        d_bn = qjs_import_base64url_bignum(cx, d);
        if (d_bn == NULL) {
            goto fail;
        }

        if (!EC_KEY_set_private_key(ec, d_bn)) {
            qjs_webcrypto_error(cx, "EC_KEY_set_private_key() failed");
            goto fail;
        }

        BN_free(d_bn);
        d_bn = NULL;
    }

    pkey = EVP_PKEY_new();
    if (pkey == NULL) {
        goto fail;
    }

    if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) {
        qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed");
        goto fail_pkey;
    }

    EC_KEY_free(ec);

    JS_FreeValue(cx, x);
    JS_FreeValue(cx, y);
    JS_FreeValue(cx, d);

    return pkey;

fail_pkey:

    EVP_PKEY_free(pkey);

fail:

    EC_KEY_free(ec);

    if (x_bn != NULL) {
        BN_free(x_bn);
    }

    if (y_bn != NULL) {
        BN_free(y_bn);
    }

    if (d_bn != NULL) {
        BN_free(d_bn);
    }

    JS_FreeValue(cx, x);
    JS_FreeValue(cx, y);
    JS_FreeValue(cx, d);

    return NULL;
}


static EVP_PKEY *
qjs_import_raw_ec(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key)
{
    EC_KEY          *ec;
    EVP_PKEY        *pkey;
    EC_POINT        *pub;
    const EC_GROUP  *group;

    ec = EC_KEY_new_by_curve_name(key->u.a.curve);
    if (ec == NULL) {
        qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed");
        return NULL;
    }

    group = EC_KEY_get0_group(ec);

    pub = EC_POINT_new(group);
    if (pub == NULL) {
        EC_KEY_free(ec);
        qjs_webcrypto_error(cx, "EC_POINT_new() failed");
        return NULL;
    }

    if (!EC_POINT_oct2point(group, pub, data->start, data->length, NULL)) {
        EC_KEY_free(ec);
        EC_POINT_free(pub);
        qjs_webcrypto_error(cx, "EC_POINT_oct2point() failed");
        return NULL;
    }

    if (!EC_KEY_set_public_key(ec, pub)) {
        EC_KEY_free(ec);
        EC_POINT_free(pub);
        qjs_webcrypto_error(cx, "EC_KEY_set_public_key() failed");
        return NULL;
    }

    pkey = EVP_PKEY_new();
    if (pkey == NULL) {
        EC_KEY_free(ec);
        EC_POINT_free(pub);
        qjs_webcrypto_error(cx, "EVP_PKEY_new() failed");
        return NULL;
    }

    if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) {
        EC_KEY_free(ec);
        EC_POINT_free(pub);
        EVP_PKEY_free(pkey);
        qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed");
        return NULL;
    }

    EC_KEY_free(ec);
    EC_POINT_free(pub);

    return pkey;
}


#if (NJS_HAVE_ED25519)
static EVP_PKEY *
qjs_import_jwk_okp(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
{
    int         pkey_id;
    size_t      crv_len, x_len, d_len;
    JSValue     val;
    EVP_PKEY    *pkey;
    njs_str_t   x_b64, d_b64, x_raw, d_raw;
    const char  *crv_str, *x_str, *d_str;
    u_char      x_buf[32], d_buf[32];

    val = JS_GetPropertyStr(cx, jwk, "crv");
    if (!JS_IsString(val)) {
        JS_FreeValue(cx, val);
        JS_ThrowTypeError(cx, "Invalid JWK OKP crv");
        return NULL;
    }

    crv_str = JS_ToCStringLen(cx, &crv_len, val);
    JS_FreeValue(cx, val);

    if (crv_str == NULL) {
        JS_ThrowTypeError(cx, "unsupported JWK OKP curve");
        return NULL;
    }

    if (crv_len == 7 && memcmp(crv_str, "Ed25519", 7) == 0) {
        pkey_id = EVP_PKEY_ED25519;

    } else if (crv_len == 6 && memcmp(crv_str, "X25519", 6) == 0) {
        pkey_id = EVP_PKEY_X25519;

    } else {
        JS_FreeCString(cx, crv_str);
        JS_ThrowTypeError(cx, "unsupported JWK OKP curve");
        return NULL;
    }

    JS_FreeCString(cx, crv_str);

    val = JS_GetPropertyStr(cx, jwk, "x");
    if (!JS_IsString(val)) {
        JS_FreeValue(cx, val);
        JS_ThrowTypeError(cx, "Invalid JWK OKP x");
        return NULL;
    }

    x_str = JS_ToCStringLen(cx, &x_len, val);
    JS_FreeValue(cx, val);

    x_b64.start = (u_char *) x_str;
    x_b64.length = x_len;
    x_raw.start = x_buf;
    x_raw.length = qjs_base64url_decode_length(cx, &x_b64);

    if (x_raw.length != 32) {
        JS_FreeCString(cx, x_str);
        JS_ThrowTypeError(cx, "Invalid JWK OKP x length");
        return NULL;
    }

    qjs_base64url_decode(cx, &x_b64, &x_raw);
    JS_FreeCString(cx, x_str);

    val = JS_GetPropertyStr(cx, jwk, "d");

    if (JS_IsString(val)) {
        d_str = JS_ToCStringLen(cx, &d_len, val);
        JS_FreeValue(cx, val);

        d_b64.start = (u_char *) d_str;
        d_b64.length = d_len;
        d_raw.start = d_buf;
        d_raw.length = qjs_base64url_decode_length(cx, &d_b64);

        if (d_raw.length != 32) {
            JS_FreeCString(cx, d_str);
            JS_ThrowTypeError(cx, "Invalid JWK OKP d length");
            return NULL;
        }

        qjs_base64url_decode(cx, &d_b64, &d_raw);
        JS_FreeCString(cx, d_str);

        pkey = EVP_PKEY_new_raw_private_key(pkey_id, NULL, d_buf, 32);
        key->u.a.privat = 1;

    } else {
        JS_FreeValue(cx, val);
        pkey = EVP_PKEY_new_raw_public_key(pkey_id, NULL, x_buf, 32);
    }

    if (pkey == NULL) {
        qjs_webcrypto_error(cx, "EVP_PKEY_new_raw_*_key() failed");
        return NULL;
    }

    return pkey;
}
#endif


static JSValue
qjs_import_jwk_oct(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key)
{
    size_t                 size;
    unsigned               usage;
    JSValue                val, ret;
    njs_str_t              *a, alg, b64;
    qjs_webcrypto_alg_t    type;
    qjs_webcrypto_entry_t  *w;

    static qjs_webcrypto_entry_t hashes[] = {
        { njs_str("HS1"), QJS_HASH_SHA1 },
        { njs_str("HS256"), QJS_HASH_SHA256 },
        { njs_str("HS384"), QJS_HASH_SHA384 },
        { njs_str("HS512"), QJS_HASH_SHA512 },
        { njs_null_str, 0 }
    };

    val = JS_GetPropertyStr(cx, jwk, "k");
    if (JS_IsException(val) || !JS_IsString(val)) {
        JS_ThrowTypeError(cx, "Invalid JWK oct key");
        return JS_EXCEPTION;
    }

    b64.start = (u_char *) JS_ToCStringLen(cx, &b64.length, val);
    JS_FreeValue(cx, val);
    if (b64.start == NULL) {
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    size = qjs_base64url_decode_length(cx, &b64);

    key->u.s.raw.length = size;
    key->u.s.raw.start = js_malloc(cx, size);
    if (key->u.s.raw.start == NULL) {
        JS_FreeCString(cx, (char *) b64.start);
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    qjs_base64url_decode(cx, &b64, &key->u.s.raw);
    JS_FreeCString(cx, (char *) b64.start);

    val = JS_GetPropertyStr(cx, jwk, "alg");
    if (JS_IsException(val)) {
        return JS_EXCEPTION;
    }

    if (!JS_IsString(val)) {
        JS_FreeValue(cx, val);
        return JS_ThrowTypeError(cx, "Invalid JWK oct alg");
    }

    alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, val);
    JS_FreeValue(cx, val);
    if (alg.start == NULL) {
        JS_ThrowOutOfMemory(cx);
        return JS_EXCEPTION;
    }

    size = 16;

    if (key->alg->type == QJS_ALGORITHM_HMAC) {
        for (w = &hashes[0]; w->name.length != 0; w++) {
            if (njs_strstr_eq(&alg, &w->name)) {
                key->hash = w->value;
                goto done;
            }
        }

    } else {
        type = key->alg->type;
        a = &qjs_webcrypto_alg_aes_name[type - QJS_ALGORITHM_AES_GCM][0];
        for (; a->length != 0; a++) {
            if (njs_strstr_eq(&alg, a)) {
                goto done;
            }

            size += 8;
        }
    }

    JS_ThrowTypeError(cx, "unexpected \"alg\" value \"%s\" for JWK key",
                      alg.start);
    JS_FreeCString(cx, (char *) alg.start);
    return JS_EXCEPTION;

done:

    if (key->alg->type != QJS_ALGORITHM_HMAC) {
        if (key->u.s.raw.length != size) {
            JS_ThrowTypeError(cx, "key size and \"alg\" value \"%s\" mismatch",
                              alg.start);
            JS_FreeCString(cx, (char *) alg.start);
            return JS_EXCEPTION;
        }
    }

    JS_FreeCString(cx, (char *) alg.start);

    val = JS_GetPropertyStr(cx, jwk, "key_ops");
    if (!JS_IsException(val) && !JS_IsUndefined(val)) {
        ret = qjs_key_usage(cx, val, &usage);
        JS_FreeValue(cx, val);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

        if ((key->usage & usage) != key->usage) {
            JS_ThrowTypeError(cx, "Key operations and usage mismatch");
            return JS_EXCEPTION;
        }
    }

    if (key->extractable) {
        val = JS_GetPropertyStr(cx, jwk, "ext");
        if (!JS_IsException(val)
            && !JS_IsUndefined(val)
            && !JS_ToBool(cx, val))
        {
            JS_FreeValue(cx, val);
            JS_ThrowTypeError(cx, "JWK oct is not extractable");
            return JS_EXCEPTION;
        }

        JS_FreeValue(cx, val);
    }

    return JS_UNDEFINED;
}


static JSValue
qjs_webcrypto_import_key_internal(JSContext *cx, qjs_webcrypto_key_format_t fmt,
    JSValueConst key_data, qjs_webcrypto_algorithm_t *alg, JSValueConst options,
    int extractable, unsigned usage)
{
    int                      nid;
    BIO                      *bio;
#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
    RSA                      *rsa;
    EC_KEY                   *ec;
#else
    char                     gname[80];
#endif
    JSValue                  key, jwk, val, ret;
    unsigned                 mask;
    EVP_PKEY                 *pkey;
    njs_str_t                kd;
    const u_char             *start;
#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
    const EC_GROUP           *group;
#endif
    qjs_webcrypto_key_t      *wkey;
    PKCS8_PRIV_KEY_INFO      *pkcs8;
    qjs_webcrypto_hash_t     hash;
    qjs_webcrypto_jwk_kty_t  kty;

    pkey = NULL;
    kd.start = NULL;
    kd.length = 0;

    if (usage & ~alg->usage) {
        JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key",
                          qjs_algorithm_string(alg));
        return JS_EXCEPTION;
    }

    if (!(fmt & alg->fmt)) {
        JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key",
                          qjs_format_string(fmt), qjs_algorithm_string(alg));
        return JS_EXCEPTION;
    }

    if (fmt != QJS_KEY_FORMAT_JWK) {
        ret = qjs_typed_array_data(cx, key_data, &kd);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }
    }

    key = qjs_webcrypto_key_make(cx, alg, usage, extractable);
    if (JS_IsException(key)) {
        return JS_EXCEPTION;
    }

    wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);

    switch (fmt) {
    case QJS_KEY_FORMAT_PKCS8:
        bio = BIO_new_mem_buf(kd.start, kd.length);
        if (bio == NULL) {
            qjs_webcrypto_error(cx, "BIO_new_mem_buf() failed");
            goto fail;
        }

        pkcs8 = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL);
        if (pkcs8 == NULL) {
            BIO_free(bio);
            qjs_webcrypto_error(cx, "d2i_PKCS8_PRIV_KEY_INFO_bio() failed");
            goto fail;
        }

        pkey = EVP_PKCS82PKEY(pkcs8);
        if (pkey == NULL) {
            PKCS8_PRIV_KEY_INFO_free(pkcs8);
            BIO_free(bio);
            qjs_webcrypto_error(cx, "EVP_PKCS82PKEY() failed");
            goto fail;
        }

        PKCS8_PRIV_KEY_INFO_free(pkcs8);
        BIO_free(bio);

        wkey->u.a.privat = 1;

        break;

    case QJS_KEY_FORMAT_SPKI:
        start = kd.start;
        pkey = d2i_PUBKEY(NULL, &start, kd.length);
        if (pkey == NULL) {
            qjs_webcrypto_error(cx, "d2i_PUBKEY() failed");
            goto fail;
        }

        break;

    case QJS_KEY_FORMAT_JWK:
        jwk = key_data;
        if (!JS_IsObject(jwk)) {
            JS_ThrowTypeError(cx, "invalid JWK key data: object value "
                              "expected");
            goto fail;
        }

        val = JS_GetPropertyStr(cx, jwk, "kty");
        if (JS_IsException(val)) {
            goto fail;
        }

        kty = qjs_jwk_kty(cx, val);
        JS_FreeValue(cx, val);
        if (kty == QJS_KEY_JWK_KTY_UNKNOWN) {
            goto fail;
        }

        switch (kty) {
        case QJS_KEY_JWK_KTY_RSA:
            if (alg->type != QJS_ALGORITHM_RSASSA_PKCS1_v1_5
                && alg->type != QJS_ALGORITHM_RSA_PSS
                && alg->type != QJS_ALGORITHM_RSA_OAEP)
            {
                JS_ThrowTypeError(cx, "JWK kty \"RSA\" doesn't "
                                  "match algorithm \"%s\"",
                                  qjs_algorithm_string(alg));
                goto fail;
            }

            pkey = qjs_import_jwk_rsa(cx, jwk, wkey);
            if (pkey == NULL) {
                goto fail;
            }

            break;

        case QJS_KEY_JWK_KTY_EC:
            if (alg->type != QJS_ALGORITHM_ECDSA
                && alg->type != QJS_ALGORITHM_ECDH)
            {
                JS_ThrowTypeError(cx, "JWK kty \"EC\" doesn't "
                                  "match algorithm \"%s\"",
                                  qjs_algorithm_string(alg));
                goto fail;
            }

            ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
            if (JS_IsException(ret)) {
                goto fail;
            }

            pkey = qjs_import_jwk_ec(cx, jwk, wkey);
            if (pkey == NULL) {
                goto fail;
            }

            break;

        case QJS_KEY_JWK_KTY_OCT:
            if (!alg->raw) {
                JS_ThrowTypeError(cx, "JWK kty \"oct\" doesn't "
                                  "match algorithm \"%s\"",
                                  qjs_algorithm_string(alg));
                goto fail;
            }

            ret = qjs_import_jwk_oct(cx, jwk, wkey);
            if (JS_IsException(ret)) {
                goto fail;
            }

            break;

#if (NJS_HAVE_ED25519)
        case QJS_KEY_JWK_KTY_OKP:
            if (alg->type != QJS_ALGORITHM_ED25519
                && alg->type != QJS_ALGORITHM_X25519)
            {
                JS_ThrowTypeError(cx, "JWK kty \"OKP\" doesn't "
                                  "match algorithm \"%s\"",
                                  qjs_algorithm_string(alg));
                goto fail;
            }

            pkey = qjs_import_jwk_okp(cx, jwk, wkey);
            if (pkey == NULL) {
                goto fail;
            }

            break;
#endif

        default:
            JS_ThrowTypeError(cx, "unsupported JWK kty");
            goto fail;
        }

        break;

    case QJS_KEY_FORMAT_RAW:
    default:
        break;
    }

    switch (alg->type) {
    case QJS_ALGORITHM_RSA_OAEP:
    case QJS_ALGORITHM_RSA_PSS:
    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:

#if (OPENSSL_VERSION_NUMBER < 0x30000000L)

        rsa = EVP_PKEY_get1_RSA(pkey);
        if (rsa == NULL) {
            qjs_webcrypto_error(cx, "RSA key is not found");
            goto fail;
        }

        RSA_free(rsa);

#else
        if (!EVP_PKEY_is_a(pkey, "RSA")) {
            qjs_webcrypto_error(cx, "RSA key is not found");
            goto fail;
        }
#endif

        ret = qjs_algorithm_hash(cx, options, &hash);
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) {
            JS_ThrowTypeError(cx, "RSA JWK hash mismatch");
            goto fail;
        }

        if (wkey->u.a.privat) {
            mask = (alg->type == QJS_ALGORITHM_RSA_OAEP)
                         ? ~(QJS_KEY_USAGE_DECRYPT | QJS_KEY_USAGE_UNWRAP_KEY)
                         : ~(QJS_KEY_USAGE_SIGN);
        } else {
            mask = (alg->type == QJS_ALGORITHM_RSA_OAEP)
                         ? ~(QJS_KEY_USAGE_ENCRYPT | QJS_KEY_USAGE_WRAP_KEY)
                         : ~(QJS_KEY_USAGE_VERIFY);
        }

        if (wkey->usage & mask) {
            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
                              qjs_algorithm_string(alg));
            goto fail;
        }

        wkey->hash = hash;
        wkey->u.a.pkey = pkey;

        break;

    case QJS_ALGORITHM_ECDSA:
    case QJS_ALGORITHM_ECDH:
        ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve);
        if (JS_IsException(ret)) {
            goto fail;
        }

        if (fmt == QJS_KEY_FORMAT_RAW) {
            pkey = qjs_import_raw_ec(cx, &kd, wkey);
            if (pkey == NULL) {
                goto fail;
            }
        }

#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
        ec = EVP_PKEY_get1_EC_KEY(pkey);
        if (ec == NULL) {
            qjs_webcrypto_error(cx, "EC key is not found");
            goto fail;
        }

        group = EC_KEY_get0_group(ec);
        nid = EC_GROUP_get_curve_name(group);
        EC_KEY_free(ec);
#else
        if (!EVP_PKEY_is_a(pkey, "EC")) {
            qjs_webcrypto_error(cx, "EC key is not found");
            goto fail;
        }

        if (EVP_PKEY_get_group_name(pkey, gname, sizeof(gname), NULL) != 1) {
            qjs_webcrypto_error(cx, "EVP_PKEY_get_group_name() failed");
            goto fail;
        }

        nid = OBJ_txt2nid(gname);
#endif

        if (wkey->u.a.curve != nid) {
            qjs_webcrypto_error(cx, "name curve mismatch");
            goto fail;
        }

        if (alg->type == QJS_ALGORITHM_ECDSA) {
            mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN
                                    : ~QJS_KEY_USAGE_VERIFY;
        } else {
            if (wkey->u.a.privat) {
                mask = ~(QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS);

            } else {
                mask = 0;
            }
        }

        if (wkey->usage & mask) {
            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
                              qjs_algorithm_string(alg));
            goto fail;
        }

        wkey->u.a.pkey = pkey;
        break;

#if (NJS_HAVE_ED25519)
    case QJS_ALGORITHM_ED25519:
        if (fmt == QJS_KEY_FORMAT_RAW) {
            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
                                               kd.start, kd.length);
            if (pkey == NULL) {
                qjs_webcrypto_error(cx,
                    "EVP_PKEY_new_raw_public_key() failed");
                goto fail;
            }
        }

        if (EVP_PKEY_id(pkey) != EVP_PKEY_ED25519) {
            JS_ThrowTypeError(cx, "Ed25519 key is not found");
            goto fail;
        }

        mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY;

        if (wkey->usage & mask) {
            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
                              qjs_algorithm_string(alg));
            goto fail;
        }

        wkey->u.a.pkey = pkey;
        break;

    case QJS_ALGORITHM_X25519:
        if (fmt == QJS_KEY_FORMAT_RAW) {
            pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL,
                                               kd.start, kd.length);
            if (pkey == NULL) {
                qjs_webcrypto_error(cx,
                    "EVP_PKEY_new_raw_public_key() failed");
                goto fail;
            }
        }

        if (EVP_PKEY_id(pkey) != EVP_PKEY_X25519) {
            JS_ThrowTypeError(cx, "X25519 key is not found");
            goto fail;
        }

        if (wkey->u.a.privat) {
            mask = ~(QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS);
        } else {
            mask = 0;
        }

        if (wkey->usage & mask) {
            JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key",
                              qjs_algorithm_string(alg));
            goto fail;
        }

        wkey->u.a.pkey = pkey;
        break;
#endif

    case QJS_ALGORITHM_HMAC:
        if (fmt == QJS_KEY_FORMAT_RAW) {
            ret = qjs_algorithm_hash(cx, options, &wkey->hash);
            if (JS_IsException(ret)) {
                goto fail;
            }

            wkey->u.s.raw.start = js_malloc(cx, kd.length);
            if (wkey->u.s.raw.start == NULL) {
                JS_ThrowOutOfMemory(cx);
                goto fail;
            }

            wkey->u.s.raw.length = kd.length;
            memcpy(wkey->u.s.raw.start, kd.start, kd.length);

        } else {
            /* QJS_KEY_FORMAT_JWK. */

            ret = qjs_algorithm_hash(cx, options, &hash);
            if (JS_IsException(ret)) {
                goto fail;
            }

            if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) {
                JS_ThrowTypeError(cx, "HMAC JWK hash mismatch");
                goto fail;
            }
        }

        break;

    case QJS_ALGORITHM_AES_GCM:
    case QJS_ALGORITHM_AES_CTR:
    case QJS_ALGORITHM_AES_CBC:
    case QJS_ALGORITHM_AES_KW:
        if (fmt == QJS_KEY_FORMAT_RAW) {
            switch (kd.length) {
            case 16:
            case 24:
            case 32:
                break;

            default:
                JS_ThrowTypeError(cx, "AES Invalid key length");
                goto fail;
            }

            wkey->u.s.raw.start = js_malloc(cx, kd.length);
            if (wkey->u.s.raw.start == NULL) {
                JS_ThrowOutOfMemory(cx);
                goto fail;
            }

            wkey->u.s.raw.length = kd.length;
            memcpy(wkey->u.s.raw.start, kd.start, kd.length);
        }

        break;

    case QJS_ALGORITHM_PBKDF2:
    case QJS_ALGORITHM_HKDF:
    default:
        wkey->u.s.raw.start = js_malloc(cx, kd.length);
        if (wkey->u.s.raw.start == NULL) {
            JS_ThrowOutOfMemory(cx);
            goto fail;
        }

        wkey->u.s.raw.length = kd.length;
        memcpy(wkey->u.s.raw.start, kd.start, kd.length);
        break;
    }

    return key;

fail:

    if (pkey != NULL) {
        EVP_PKEY_free(pkey);
    }

    JS_FreeValue(cx, key);

    return JS_EXCEPTION;
}


static JSValue
qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue                     ret, key;
    unsigned                    usage;
    qjs_webcrypto_algorithm_t   *alg;
    qjs_webcrypto_key_format_t  fmt;

    fmt = qjs_key_format(cx, argv[0]);
    if (fmt == QJS_KEY_FORMAT_UNKNOWN) {
        return JS_EXCEPTION;
    }

    alg = qjs_key_algorithm(cx, argv[2]);
    if (alg == NULL) {
        return JS_EXCEPTION;
    }

    ret = qjs_key_usage(cx, argv[4], &usage);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    key = qjs_webcrypto_import_key_internal(cx, fmt, argv[1], alg, argv[2],
                                            JS_ToBool(cx, argv[3]), usage);
    if (JS_IsException(key)) {
        return qjs_promise_result(cx, JS_EXCEPTION);
    }

    return qjs_promise_result(cx, key);
}


static int
qjs_set_rsa_padding(JSContext *cx, JSValue options, EVP_PKEY *pkey,
    EVP_PKEY_CTX *ctx, qjs_webcrypto_alg_t type)
{
    int      padding, rc;
    int64_t  salt_length;
    JSValue  value;

    if (type == QJS_ALGORITHM_ECDSA) {
        return 0;
    }

    padding = (type == QJS_ALGORITHM_RSA_PSS) ? RSA_PKCS1_PSS_PADDING
                                              : RSA_PKCS1_PADDING;
    rc = EVP_PKEY_CTX_set_rsa_padding(ctx, padding);
    if (rc <= 0) {
        qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_padding() failed");
        return -1;
    }

    if (padding == RSA_PKCS1_PSS_PADDING) {
        value = JS_GetPropertyStr(cx, options, "saltLength");
        if (JS_IsException(value)) {
            return -1;
        }

        if (JS_IsUndefined(value)) {
            JS_ThrowTypeError(cx, "RSA-PSS algorithm.saltLength is not "
                              "provided");
            return -1;
        }

        rc = JS_ToInt64(cx, &salt_length, value);
        JS_FreeValue(cx, value);
        if (rc < 0) {
            return -1;
        }

        rc = EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_length);
        if (rc <= 0) {
            qjs_webcrypto_error(cx,
                                "EVP_PKEY_CTX_set_rsa_pss_saltlen() failed");
            return -1;
        }
    }

    return 0;
}


static unsigned int
qjs_ec_rs_size(EVP_PKEY *pkey)
{
    int             bits;
    const EC_KEY    *ec_key;
    const EC_GROUP  *ec_group;

    ec_key = njs_pkey_get_ec_key(pkey);
    if (ec_key == NULL) {
        return 0;
    }

    ec_group = EC_KEY_get0_group(ec_key);
    if (ec_group == NULL) {
        return 0;
    }

    bits = njs_ec_group_order_bits(ec_group);
    if (bits == 0) {
        return 0;
    }

    return (bits + 7) / 8;
}


static int
qjs_convert_der_to_p1363(JSContext *cx, EVP_PKEY *pkey, const u_char *der,
    size_t der_len, u_char **pout, size_t *out_len)
{
    u_char        *data;
    unsigned      n;
    ECDSA_SIG     *ec_sig;
    const BIGNUM  *r, *s;

    ec_sig = NULL;

    n = qjs_ec_rs_size(pkey);
    if (n == 0) {
        return -1;
    }

    data = js_malloc(cx, 2 * n);
    if (data == NULL) {
        JS_ThrowOutOfMemory(cx);
        return -1;
    }

    ec_sig = d2i_ECDSA_SIG(NULL, &der, der_len);
    if (ec_sig == NULL) {
        goto fail;
    }

#if (OPENSSL_VERSION_NUMBER >= 0x10100000L)
    ECDSA_SIG_get0(ec_sig, &r, &s);
#else
    r = ec_sig->r;
    s = ec_sig->s;
#endif

    if (BN_bn2binpad(r, data, n) <= 0) {
        goto fail;
    }

    if (BN_bn2binpad(s, &data[n], n) <= 0) {
        goto fail;
    }

    *pout = data;
    *out_len = 2 * n;

    ECDSA_SIG_free(ec_sig);

    return 0;

fail:

    js_free(cx, data);

    if (ec_sig != NULL) {
        ECDSA_SIG_free(ec_sig);
    }

    return -1;
}


static int
qjs_convert_p1363_to_der(JSContext *cx, EVP_PKEY *pkey, u_char *p1363,
    size_t p1363_len, u_char **pout, size_t *out_len)
{
    int        len;
    BIGNUM     *r, *s;
    u_char     *data;
    unsigned   n;
    ECDSA_SIG  *ec_sig;

    n = qjs_ec_rs_size(pkey);

    if (n == 0 || p1363_len != 2 * n) {
        JS_ThrowTypeError(cx, "invalid ECDSA signature length %zu != %u",
                          p1363_len, 2 * n);
        return -1;
    }

    ec_sig = ECDSA_SIG_new();
    if (ec_sig == NULL) {
        JS_ThrowOutOfMemory(cx);
        return -1;
    }

    r = BN_bin2bn(p1363, n, NULL);
    if (r == NULL) {
        JS_ThrowOutOfMemory(cx);
        goto fail;
    }

    s = BN_bin2bn(&p1363[n], n, NULL);
    if (s == NULL) {
        BN_free(r);
        JS_ThrowOutOfMemory(cx);
        goto fail;
    }

    if (ECDSA_SIG_set0(ec_sig, r, s) != 1) {
        BN_free(r);
        BN_free(s);
        JS_ThrowOutOfMemory(cx);
        goto fail;
    }

    data = js_malloc(cx, 2 * n + 16);
    if (data == NULL) {
        JS_ThrowOutOfMemory(cx);
        goto fail;
    }

    *pout = data;
    len = i2d_ECDSA_SIG(ec_sig, &data);

    if (len < 0) {
        js_free(cx, data);
        qjs_webcrypto_error(cx, "i2d_ECDSA_SIG() failed");
        goto fail;
    }

    *out_len = len;

    ECDSA_SIG_free(ec_sig);

    return 0;

fail:

    ECDSA_SIG_free(ec_sig);

    return -1;
}


static JSValue
qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv, int verify)
{
    int                        rc;
    u_char                     *dst, *p, *p1363;
    size_t                     olen, outlen;
    JSValue                    ret, options;
    unsigned                   mask, m_len;
    njs_str_t                  data, sig;
    EVP_MD_CTX                 *mctx;
    EVP_PKEY_CTX               *pctx;
    const EVP_MD               *md;
    qjs_webcrypto_key_t        *key;
    qjs_webcrypto_hash_t       hash;
    qjs_webcrypto_algorithm_t  *alg;
    unsigned char              m[EVP_MAX_MD_SIZE];

    dst = NULL;
    mctx = NULL;
    pctx = NULL;

    options = argv[0];

    alg = qjs_key_algorithm(cx, options);
    if (alg == NULL) {
        return JS_EXCEPTION;
    }

    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    mask = verify ? QJS_KEY_USAGE_VERIFY : QJS_KEY_USAGE_SIGN;
    if (!(key->usage & mask)) {
        JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation",
                          verify ? "verify" : "sign");
        return JS_EXCEPTION;
    }

    if (key->alg != alg) {
        JS_ThrowTypeError(cx, "cannot %s using \"%s\" with \"%s\" key",
                          verify ? "verify" : "sign",
                          qjs_algorithm_string(key->alg),
                          qjs_algorithm_string(alg));
        return JS_EXCEPTION;
    }

    if (verify) {
        ret = qjs_typed_array_data(cx, argv[2], &sig);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

        ret = qjs_typed_array_data(cx, argv[3], &data);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

    } else {
        ret = qjs_typed_array_data(cx, argv[2], &data);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }
    }

    md = NULL;

    if (alg->type == QJS_ALGORITHM_ECDSA) {
        ret = qjs_algorithm_hash(cx, options, &hash);
        if (JS_IsException(ret)) {
            return JS_EXCEPTION;
        }

        md = qjs_algorithm_hash_digest(hash);

    } else if (alg->type != QJS_ALGORITHM_ED25519) {
        hash = key->hash;
        md = qjs_algorithm_hash_digest(hash);
    }

    /* Clang complains about uninitialized rc. */
    rc = 0;
    outlen = 0;

    switch (alg->type) {
#if (NJS_HAVE_ED25519)
    case QJS_ALGORITHM_ED25519:
        mctx = njs_evp_md_ctx_new();
        if (mctx == NULL) {
            qjs_webcrypto_error(cx, "njs_evp_md_ctx_new() failed");
            goto fail;
        }

        if (!verify) {
            if (EVP_DigestSignInit(mctx, NULL, NULL, NULL,
                                   key->u.a.pkey) <= 0)
            {
                qjs_webcrypto_error(cx,
                                    "EVP_DigestSignInit() failed");
                goto fail;
            }

            outlen = 0;
            if (EVP_DigestSign(mctx, NULL, &outlen, data.start,
                               data.length) <= 0)
            {
                qjs_webcrypto_error(cx, "EVP_DigestSign() failed");
                goto fail;
            }

            dst = js_malloc(cx, outlen);
            if (dst == NULL) {
                JS_ThrowOutOfMemory(cx);
                goto fail;
            }

            if (EVP_DigestSign(mctx, dst, &outlen, data.start,
                               data.length) <= 0)
            {
                qjs_webcrypto_error(cx, "EVP_DigestSign() failed");
                goto fail;
            }

        } else {
            if (EVP_DigestVerifyInit(mctx, NULL, NULL, NULL,
                                     key->u.a.pkey) <= 0)
            {
                qjs_webcrypto_error(cx,
                                    "EVP_DigestVerifyInit() failed");
                goto fail;
            }

            rc = EVP_DigestVerify(mctx, sig.start, sig.length,
                                  data.start, data.length);
            if (rc < 0) {
                qjs_webcrypto_error(cx,
                                    "EVP_DigestVerify() failed");
                goto fail;
            }
        }

        njs_evp_md_ctx_free(mctx);
        mctx = NULL;

        break;
#endif

    case QJS_ALGORITHM_HMAC:
        m_len = EVP_MD_size(md);

        if (!verify) {
            dst = js_malloc(cx, m_len);
            if (dst == NULL) {
                JS_ThrowOutOfMemory(cx);
                return JS_EXCEPTION;
            }

        } else {
            dst = (u_char *) &m[0];
        }

        outlen = m_len;

        p = HMAC(md, key->u.s.raw.start, key->u.s.raw.length, data.start,
                 data.length, dst, &m_len);

        if (p == NULL || m_len != outlen) {
            qjs_webcrypto_error(cx, "HMAC() failed");
            goto fail;
        }

        if (verify) {
            rc = (sig.length == outlen && memcmp(sig.start, dst, outlen) == 0);
        }

        break;

    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
    case QJS_ALGORITHM_RSA_PSS:
    case QJS_ALGORITHM_ECDSA:
    default:
        mctx = njs_evp_md_ctx_new();
        if (mctx == NULL) {
            qjs_webcrypto_error(cx, "njs_evp_md_ctx_new() failed");
            goto fail;
        }

        rc = EVP_DigestInit_ex(mctx, md, NULL);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_DigestInit_ex() failed");
            goto fail;
        }

        rc = EVP_DigestUpdate(mctx, data.start, data.length);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_DigestUpdate() failed");
            goto fail;
        }

        rc = EVP_DigestFinal_ex(mctx, m, &m_len);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_DigestFinal_ex() failed");
            goto fail;
        }

        olen = EVP_PKEY_size(key->u.a.pkey);
        dst = js_malloc(cx, olen);
        if (dst == NULL) {
            JS_ThrowOutOfMemory(cx);
            goto fail;
        }

        pctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL);
        if (pctx == NULL) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed");
            goto fail;
        }

        if (!verify) {
            rc = EVP_PKEY_sign_init(pctx);
            if (rc <= 0) {
                qjs_webcrypto_error(cx, "EVP_PKEY_sign_init() failed");
                goto fail;
            }

        } else {
            rc = EVP_PKEY_verify_init(pctx);
            if (rc <= 0) {
                qjs_webcrypto_error(cx, "EVP_PKEY_verify_init() failed");
                goto fail;
            }
        }

        rc = qjs_set_rsa_padding(cx, options, key->u.a.pkey, pctx, alg->type);
        if (rc < 0) {
            goto fail;
        }

        rc = EVP_PKEY_CTX_set_signature_md(pctx, md);
        if (rc <= 0) {
            qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_signature_md() failed");
            goto fail;
        }

        if (!verify) {
            outlen = olen;
            rc = EVP_PKEY_sign(pctx, dst, &outlen, m, m_len);
            if (rc <= 0) {
                qjs_webcrypto_error(cx, "EVP_PKEY_sign() failed");
                goto fail;
            }

            if (alg->type == QJS_ALGORITHM_ECDSA) {
                rc = qjs_convert_der_to_p1363(cx, key->u.a.pkey, dst, outlen,
                                              &p1363, &outlen);
                if (rc < 0) {
                    goto fail;
                }

                js_free(cx, dst);
                dst = p1363;
            }

        } else {
            if (alg->type == QJS_ALGORITHM_ECDSA) {
                rc = qjs_convert_p1363_to_der(cx, key->u.a.pkey, sig.start,
                                              sig.length, &sig.start,
                                              &sig.length);
                if (rc < 0) {
                    goto fail;
                }
            }

            rc = EVP_PKEY_verify(pctx, sig.start, sig.length, m, m_len);

            if (alg->type == QJS_ALGORITHM_ECDSA) {
                js_free(cx, sig.start);
            }

            if (rc < 0) {
                qjs_webcrypto_error(cx, "EVP_PKEY_verify() failed");
                goto fail;
            }

            js_free(cx, dst);
        }

        njs_evp_md_ctx_free(mctx);
        mctx = NULL;

        EVP_PKEY_CTX_free(pctx);
        pctx = NULL;
    }

    if (!verify) {
        ret = qjs_new_array_buffer(cx, dst, outlen);
        if (JS_IsException(ret)) {
            goto fail;
        }

    } else {
        ret = JS_NewBool(cx, rc != 0);
    }

    return qjs_promise_result(cx, ret);

fail:

    if (mctx != NULL) {
        njs_evp_md_ctx_free(mctx);
    }

    if (pctx != NULL) {
        EVP_PKEY_CTX_free(pctx);
    }

    if (dst != NULL) {
        js_free(cx, dst);
    }

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_webcrypto_wrap_key(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    unsigned                    mask;
    JSValue                     exported, options, ret;
    njs_str_t                   data;
    qjs_webcrypto_key_t         *key, *wrapping_key;
    qjs_webcrypto_algorithm_t   *alg;
    qjs_webcrypto_key_format_t  fmt;

    fmt = qjs_key_format(cx, argv[0]);

    key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        goto fail;
    }

    wrapping_key = JS_GetOpaque2(cx, argv[2],
                                 QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (wrapping_key == NULL) {
        JS_ThrowTypeError(cx, "\"wrappingKey\" is not a CryptoKey object");
        goto fail;
    }

    options = argv[3];
    alg = qjs_key_algorithm(cx, options);
    if (alg == NULL) {
        goto fail;
    }

    if (!key->extractable) {
        JS_ThrowTypeError(cx, "provided key cannot be extracted");
        goto fail;
    }

    mask = QJS_KEY_USAGE_WRAP_KEY;
    if (!(wrapping_key->usage & mask)) {
        JS_ThrowTypeError(cx, "wrapping key does not support wrapKey");
        goto fail;
    }

    if (wrapping_key->alg != alg) {
        JS_ThrowTypeError(cx, "cannot wrap using \"%s\" with \"%s\" key",
                          qjs_algorithm_string(wrapping_key->alg),
                          qjs_algorithm_string(alg));
        goto fail;
    }

    exported = qjs_webcrypto_export_key_raw(cx, key, fmt);
    if (JS_IsException(exported)) {
        goto fail;
    }

    if (fmt == QJS_KEY_FORMAT_JWK) {
        JSValue       json;
        const char    *str;

        json = JS_JSONStringify(cx, exported, JS_UNDEFINED, JS_UNDEFINED);
        JS_FreeValue(cx, exported);

        if (JS_IsException(json)) {
            goto fail;
        }

        str = JS_ToCStringLen(cx, &data.length, json);
        JS_FreeValue(cx, json);

        if (str == NULL) {
            goto fail;
        }

        data.start = (u_char *) str;

        ret = qjs_webcrypto_cipher_core(cx, &data, wrapping_key, options,
                                        alg, 1);
        JS_FreeCString(cx, str);

    } else {
        ret = qjs_typed_array_data(cx, exported, &data);
        if (JS_IsException(ret)) {
            JS_FreeValue(cx, exported);
            goto fail;
        }

        ret = qjs_webcrypto_cipher_core(cx, &data, wrapping_key, options,
                                        alg, 1);
        JS_FreeValue(cx, exported);
    }

    if (JS_IsException(ret)) {
        goto fail;
    }

    return qjs_promise_result(cx, ret);

fail:

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_webcrypto_unwrap_key(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    unsigned                    usage;
    JSValue                     options, ret, decrypted, key_value;
    njs_str_t                   data, key_data;
    const char                  *str;
    size_t                      len;
    JSValue                     json_str;
    qjs_webcrypto_key_t         *wrapping_key;
    qjs_webcrypto_algorithm_t   *alg, *key_alg;
    qjs_webcrypto_key_format_t  fmt;

    fmt = qjs_key_format(cx, argv[0]);

    ret = qjs_typed_array_data(cx, argv[1], &data);
    if (JS_IsException(ret)) {
        return JS_EXCEPTION;
    }

    wrapping_key = JS_GetOpaque2(cx, argv[2],
                                 QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (wrapping_key == NULL) {
        JS_ThrowTypeError(cx, "\"unwrappingKey\" is not a CryptoKey object");
        goto fail;
    }

    options = argv[3];
    alg = qjs_key_algorithm(cx, options);
    if (alg == NULL) {
        goto fail;
    }

    key_alg = qjs_key_algorithm(cx, argv[4]);
    if (key_alg == NULL) {
        goto fail;
    }

    ret = qjs_key_usage(cx, argv[6], &usage);
    if (JS_IsException(ret)) {
        goto fail;
    }

    if (!(wrapping_key->usage & QJS_KEY_USAGE_UNWRAP_KEY)) {
        JS_ThrowTypeError(cx, "unwrapping key does not support unwrapKey");
        goto fail;
    }

    if (wrapping_key->alg != alg) {
        JS_ThrowTypeError(cx, "cannot unwrap using \"%s\" with \"%s\" key",
                          qjs_algorithm_string(wrapping_key->alg),
                          qjs_algorithm_string(alg));
        goto fail;
    }

    decrypted = qjs_webcrypto_cipher_core(cx, &data, wrapping_key, options,
                                          alg, 0);
    if (JS_IsException(decrypted)) {
        goto fail;
    }

    if (fmt == QJS_KEY_FORMAT_JWK) {
        ret = qjs_typed_array_data(cx, decrypted, &key_data);
        if (JS_IsException(ret)) {
            JS_FreeValue(cx, decrypted);
            goto fail;
        }

        json_str = JS_NewStringLen(cx, (const char *) key_data.start,
                                   key_data.length);
        JS_FreeValue(cx, decrypted);

        if (JS_IsException(json_str)) {
            goto fail;
        }

        str = JS_ToCStringLen(cx, &len, json_str);
        JS_FreeValue(cx, json_str);

        if (str == NULL) {
            goto fail;
        }

        decrypted = JS_ParseJSON(cx, str, len, "<unwrapKey>");
        JS_FreeCString(cx, str);

        if (JS_IsException(decrypted)) {
            JS_ThrowTypeError(cx, "wrapped key is not valid JWK "
                              "JSON");
            goto fail;
        }
    }

    key_value = qjs_webcrypto_import_key_internal(cx, fmt, decrypted,
                                                  key_alg, argv[4],
                                                  JS_ToBool(cx, argv[5]),
                                                  usage);
    JS_FreeValue(cx, decrypted);

    if (JS_IsException(key_value)) {
        goto fail;
    }

    return qjs_promise_result(cx, key_value);

fail:

    return qjs_promise_result(cx, JS_EXCEPTION);
}


static JSValue
qjs_webcrypto_key_algorithm(JSContext *cx, JSValueConst this_val)
{
    JSValue              obj, ret, hash, len, pe;
    njs_str_t            *name, pe_data;
    const BIGNUM         *n_bn, *e_bn;
    const EC_GROUP       *group;
    qjs_webcrypto_key_t  *key;

    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    name = &qjs_webcrypto_alg[key->alg->type].name;

    obj = JS_NewObject(cx);
    if (JS_IsException(obj)) {
        return JS_EXCEPTION;
    }

    ret = JS_NewStringLen(cx, (const char *) name->start, name->length);
    if (JS_IsException(ret)) {
        JS_FreeValue(cx, obj);
        return JS_EXCEPTION;
    }

    if (JS_DefinePropertyValueStr(cx, obj, "name", ret, JS_PROP_C_W_E)
        < 0)
    {
        JS_FreeValue(cx, obj);
        return JS_EXCEPTION;
    }

    switch (key->alg->type) {
    case QJS_ALGORITHM_RSASSA_PKCS1_v1_5:
    case QJS_ALGORITHM_RSA_PSS:
    case QJS_ALGORITHM_RSA_OAEP:
        /* RsaHashedKeyGenParams. */

        njs_assert(key->u.a.pkey != NULL);
        njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_RSA);

        njs_rsa_get0_key(njs_pkey_get_rsa_key(key->u.a.pkey), &n_bn, &e_bn,
                         NULL);

        if (JS_DefinePropertyValueStr(cx, obj, "modulusLength",
                                      JS_NewInt32(cx, BN_num_bits(n_bn)),
                                                  JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        len = JS_NewInt32(cx, BN_num_bytes(e_bn));
        pe = qjs_new_uint8_array(cx, 1, &len);
        if (JS_IsException(pe)) {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        ret = qjs_typed_array_data(cx, pe, &pe_data);
        if (JS_IsException(ret)) {
            JS_FreeValue(cx, pe);
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        BN_bn2bin(e_bn, pe_data.start);

        if (JS_DefinePropertyValueStr(cx, obj, "publicExponent", pe,
                                      JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash));
        if (JS_IsException(hash)) {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        ret = JS_NewObject(cx);
        if (JS_IsException(ret)) {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        if (JS_DefinePropertyValueStr(cx, ret, "name", hash, JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, ret);
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        if (JS_DefinePropertyValueStr(cx, obj, "hash", ret, JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        break;

    case QJS_ALGORITHM_AES_GCM:
    case QJS_ALGORITHM_AES_CTR:
    case QJS_ALGORITHM_AES_CBC:
    case QJS_ALGORITHM_AES_KW:
        /* AesKeyGenParams. */

        if (JS_DefinePropertyValueStr(cx, obj, "length",
                                      JS_NewInt32(cx, key->u.s.raw.length * 8),
                                      JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        break;

    case QJS_ALGORITHM_ECDSA:
    case QJS_ALGORITHM_ECDH:
        /* EcKeyGenParams. */

        njs_assert(key->u.a.pkey != NULL);
        njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_EC);

        group = EC_KEY_get0_group(njs_pkey_get_ec_key(key->u.a.pkey));
        name = qjs_algorithm_curve_name(EC_GROUP_get_curve_name(group));

        ret = JS_NewStringLen(cx, (const char *) name->start, name->length);
        if (JS_IsException(ret)) {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        if (JS_DefinePropertyValueStr(cx, obj, "namedCurve", ret, JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        break;

    case QJS_ALGORITHM_ED25519:
    case QJS_ALGORITHM_X25519:
        break;

    case QJS_ALGORITHM_HMAC:
    default:
         /* HmacKeyGenParams */

        hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash));
        if (JS_IsException(hash)) {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        if (JS_DefinePropertyValueStr(cx, obj, "hash", hash, JS_PROP_C_W_E)
            < 0)
        {
            JS_FreeValue(cx, obj);
            return JS_EXCEPTION;
        }

        break;
    }

    return obj;
}


static JSValue
qjs_webcrypto_key_extractable(JSContext *cx, JSValueConst this_val)
{
    qjs_webcrypto_key_t  *key;

    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    return JS_NewBool(cx, key->extractable);
}


static JSValue
qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val)
{
    qjs_webcrypto_key_t  *key;

    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    if (key->alg->raw) {
        return JS_NewString(cx, "secret");
    }

    return JS_NewString(cx, key->u.a.privat ? "private": "public");
}


static JSValue
qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val)
{
    qjs_webcrypto_key_t  *key;

    key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (key == NULL) {
        JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object");
        return JS_EXCEPTION;
    }

    return qjs_key_ops(cx, key->usage);
}


static JSValue
qjs_get_random_values(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue    buffer, ret;
    njs_str_t  fill;

    buffer = JS_DupValue(cx, argv[0]);

    ret = qjs_typed_array_data(cx, buffer, &fill);
    if (JS_IsException(ret)) {
        JS_FreeValue(cx, buffer);
        return JS_EXCEPTION;
    }

    if (fill.length > 65536) {
        JS_ThrowTypeError(cx, "requested length exceeds 65536 bytes");
        JS_FreeValue(cx, buffer);
        return JS_EXCEPTION;
    }

    if (RAND_bytes(fill.start, fill.length) != 1) {
        JS_FreeValue(cx, buffer);
        qjs_webcrypto_error(cx, "RAND_bytes() failed");
        return JS_EXCEPTION;
    }

    return buffer;
}


static JSValue
qjs_random_uuid(JSContext *cx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    u_char    *p;
    uint32_t  i;
    u_char    bytes[16], buf[36];

    static const u_char  hex[] = "0123456789abcdef";

    if (RAND_bytes(bytes, 16) != 1) {
        qjs_webcrypto_error(cx, "RAND_bytes() failed");
        return JS_EXCEPTION;
    }

    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;

    p = buf;

    for (i = 0; i < 16; i++) {
        *p++ = hex[bytes[i] >> 4];
        *p++ = hex[bytes[i] & 0x0f];

        if (i == 3 || i == 5 || i == 7 || i == 9) {
            *p++ = '-';
        }
    }

    return JS_NewStringLen(cx, (const char *) buf, sizeof(buf));
}


static JSValue
qjs_webcrypto_key_make(JSContext *cx, qjs_webcrypto_algorithm_t *alg,
  unsigned usage, int extractable)
{
    JSValue              key;
    qjs_webcrypto_key_t  *k;

    key = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);
    if (JS_IsException(key)) {
        return JS_EXCEPTION;
    }

    k = js_mallocz(cx, sizeof(qjs_webcrypto_key_t));
    if (k == NULL) {
        return JS_ThrowOutOfMemory(cx);
    }

    /*
     *  k->u.a.pkey = NULL;
     *  k->u.s.raw.length = 0;
     *  k->u.s.raw.start = NULL;
     *  k->u.a.curve = 0;
     *  k->u.a.privat = 0;
     *  k->hash = QJS_HASH_UNSET;
     */

    k->alg = alg;
    k->usage = usage;
    k->extractable = extractable;

    JS_SetOpaque(key, k);

    return key;
}


static void
qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val)
{
    qjs_webcrypto_key_t  *key;

    key = JS_GetOpaque(val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY);

    if (key != NULL) {
        if (!key->alg->raw) {
            if (key->u.a.pkey != NULL) {
                EVP_PKEY_free(key->u.a.pkey);
            }

        } else {
            if (key->u.s.raw.start != NULL) {
                js_free_rt(rt, key->u.s.raw.start);
            }
        }

        js_free_rt(rt, key);
    }
}


static qjs_webcrypto_key_format_t
qjs_key_format(JSContext *cx, JSValueConst value)
{
    njs_str_t              format;
    qjs_webcrypto_entry_t  *e;

    format.start = (u_char *) JS_ToCStringLen(cx, &format.length, value);
    if (format.start == NULL) {
        return QJS_KEY_FORMAT_UNKNOWN;
    }

    for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) {
        if (njs_strstr_eq(&format, &e->name)) {
            JS_FreeCString(cx, (char *) format.start);
            return e->value;
        }
    }

    JS_ThrowTypeError(cx, "unknown key format: \"%s\"", format.start);
    JS_FreeCString(cx, (char *) format.start);

    return QJS_KEY_FORMAT_UNKNOWN;
}


static qjs_webcrypto_jwk_kty_t
qjs_jwk_kty(JSContext *cx, JSValueConst value)
{
    njs_str_t              kty;
    qjs_webcrypto_entry_t  *e;

    kty.start = (u_char *) JS_ToCStringLen(cx, &kty.length, value);
    if (kty.start == NULL) {
        return QJS_KEY_JWK_KTY_UNKNOWN;
    }

    for (e = &qjs_webcrypto_jwk_kty[0]; e->name.length != 0; e++) {
        if (njs_strstr_eq(&kty, &e->name)) {
            JS_FreeCString(cx, (char *) kty.start);
            return e->value;
        }
    }

    JS_ThrowTypeError(cx, "invalid JWK key type: \"%s\"", kty.start);
    JS_FreeCString(cx, (char *) kty.start);

    return QJS_KEY_JWK_KTY_UNKNOWN;
}


static qjs_webcrypto_algorithm_t *
qjs_key_algorithm(JSContext *cx, JSValue options)
{
    JSValue                    v;
    njs_str_t                  a;
    qjs_webcrypto_entry_t      *e;
    qjs_webcrypto_algorithm_t  *alg;

    if (JS_IsObject(options)) {
        v = JS_GetPropertyStr(cx, options, "name");
        if (JS_IsException(v)) {
            return NULL;
        }

    } else {
        v = JS_DupValue(cx, options);
    }

    a.start = (u_char *) JS_ToCStringLen(cx, &a.length, v);
    JS_FreeValue(cx, v);
    if (a.start == NULL) {
        return NULL;
    }

    for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) {
        if (njs_strstr_case_eq(&a, &e->name)) {
            alg = (qjs_webcrypto_algorithm_t *) e->value;
            JS_FreeCString(cx, (char *) a.start);
            return alg;
        }
    }

    JS_ThrowTypeError(cx, "unknown algorithm name: \"%.*s\"", (int) a.length,
                      a.start);
    JS_FreeCString(cx, (char *) a.start);

    return NULL;
}


static JSValue
qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve)
{
    JSValue                v;
    njs_str_t              name;
    qjs_webcrypto_entry_t  *e;

    if (JS_IsObject(options)) {
        v = JS_GetPropertyStr(cx, options, "namedCurve");
        if (JS_IsException(v)) {
            return JS_EXCEPTION;
        }

    } else {
        v = JS_DupValue(cx, options);
    }

    name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v);
    JS_FreeValue(cx, v);
    if (name.start == NULL) {
        return JS_EXCEPTION;
    }

    for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
        if (njs_strstr_eq(&name, &e->name)) {
            JS_FreeCString(cx, (char *) name.start);
            *curve = e->value;
            return JS_UNDEFINED;
        }
    }

    JS_ThrowTypeError(cx, "unknown namedCurve: \"%.*s\"", (int) name.length,
                      name.start);
    JS_FreeCString(cx, (char *) name.start);

    return JS_EXCEPTION;
}


static JSValue
qjs_algorithm_hash(JSContext *cx, JSValue options, qjs_webcrypto_hash_t *hash)
{
    JSValue                v;
    njs_str_t              name;
    qjs_webcrypto_entry_t  *e;

    if (JS_IsObject(options)) {
        v = JS_GetPropertyStr(cx, options, "hash");
        if (JS_IsUndefined(v)) {
            *hash = QJS_HASH_SHA256;
            return JS_UNDEFINED;
        }

        if (JS_IsException(v)) {
            return JS_EXCEPTION;
        }

    } else {
        v = JS_DupValue(cx, options);
    }

    name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v);
    JS_FreeValue(cx, v);
    if (name.start == NULL) {
        return JS_EXCEPTION;
    }

    for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) {
        if (njs_strstr_eq(&name, &e->name)) {
            JS_FreeCString(cx, (char *) name.start);
            *hash = e->value;
            return JS_UNDEFINED;
        }
    }

    JS_ThrowTypeError(cx, "unknown hash name: \"%.*s\"", (int) name.length,
                      name.start);
    JS_FreeCString(cx, (char *) name.start);

    return JS_EXCEPTION;
}


static const EVP_MD *
qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash)
{
    switch (hash) {
    case QJS_HASH_SHA256:
        return EVP_sha256();

    case QJS_HASH_SHA384:
        return EVP_sha384();

    case QJS_HASH_SHA512:
        return EVP_sha512();

    case QJS_HASH_SHA1:
    default:
        break;
    }

    return EVP_sha1();
}


static njs_str_t *
qjs_algorithm_curve_name(int curve)
{
    qjs_webcrypto_entry_t  *e;

    for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) {
        if (e->value == (uintptr_t) curve) {
            return &e->name;
        }
    }

    return &e->name;
}


static const char *
qjs_format_string(qjs_webcrypto_key_format_t fmt)
{
    qjs_webcrypto_entry_t  *e;

    for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) {
        if (fmt == e->value) {
            break;
        }
    }

    return (const char *) e->name.start;
}


static const char *
qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm)
{
    qjs_webcrypto_entry_t      *e;
    qjs_webcrypto_algorithm_t  *alg;

    for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) {
        alg = (qjs_webcrypto_algorithm_t *) e->value;
        if (alg->type == algorithm->type) {
            break;
        }
    }

    return (const char *) e->name.start;
}


static const char *
qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash)
{
    qjs_webcrypto_entry_t  *e;

    for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) {
        if (e->value == hash) {
            break;
        }
    }

    return (const char *) e->name.start;
}


static JSValue
qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask)
{
    int64_t                length;
    JSValue                v;
    uint32_t               i;
    njs_str_t              s;
    qjs_webcrypto_entry_t  *e;

    if (!qjs_is_array(cx, value)) {
        JS_ThrowTypeError(cx, "\"keyUsages\" argument must be an Array");
        return JS_EXCEPTION;
    }

    v = JS_GetPropertyStr(cx, value, "length");
    if (JS_IsException(v)) {
        return JS_EXCEPTION;
    }

    if (JS_ToInt64(cx, &length, v) < 0) {
        JS_FreeValue(cx, v);
        return JS_EXCEPTION;
    }

    JS_FreeValue(cx, v);

    *mask = 0;

    for (i = 0; i < length; i++) {
        v = JS_GetPropertyUint32(cx, value, i);
        if (JS_IsException(v)) {
            return JS_EXCEPTION;
        }

        s.start = (u_char *) JS_ToCStringLen(cx, &s.length, v);
        JS_FreeValue(cx, v);
        if (s.start == NULL) {
            JS_ThrowOutOfMemory(cx);
            return JS_EXCEPTION;
        }

        for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) {
            if (njs_strstr_eq(&s, &e->name)) {
                *mask |= e->value;
                goto done;
            }
        }

        JS_ThrowTypeError(cx, "unknown key usage: \"%.*s\"", (int) s.length,
                          s.start);
        JS_FreeCString(cx, (char *) s.start);
        return JS_EXCEPTION;

done:

        JS_FreeCString(cx, (char *) s.start);
    }

    return JS_UNDEFINED;
}


static JSValue
qjs_key_ops(JSContext *cx, unsigned mask)
{
    uint32_t               i;
    JSValue                ops, value;
    qjs_webcrypto_entry_t  *e;

    ops = JS_NewArray(cx);
    if (JS_IsException(ops)) {
        return JS_EXCEPTION;
    }

    i = 0;

    for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) {
        if (mask & e->value) {
            value = JS_NewStringLen(cx, (const char *) e->name.start,
                                    e->name.length);
            if (JS_IsException(value)) {
                JS_FreeValue(cx, ops);
                return JS_EXCEPTION;
            }

            if (JS_SetPropertyUint32(cx, ops, i++, value) < 0) {
                JS_FreeValue(cx, ops);
                return JS_EXCEPTION;
            }
        }
    }

    return ops;
}


static u_char *
qjs_cpystrn(u_char *dst, u_char *src, size_t n)
{
    if (n == 0) {
        return dst;
    }

    while (--n) {
        *dst = *src;

        if (*dst == '\0') {
            return dst;
        }

        dst++;
        src++;
    }

    *dst = '\0';

    return dst;
}


static void
qjs_webcrypto_error(JSContext *cx, const char *fmt, ...)
{
    int            flags;
    u_char         *p, *last;
    va_list        args;
    const char     *data;
    unsigned long  n;
    u_char         errstr[NJS_MAX_ERROR_STR];

    last = &errstr[NJS_MAX_ERROR_STR];

    va_start(args, fmt);
    p = njs_vsprintf(errstr, last - 1, fmt, args);
    va_end(args);

    if (ERR_peek_error()) {
        p = qjs_cpystrn(p, (u_char *) " (SSL:", last - p);

        for ( ;; ) {

            n = ERR_peek_error_data(&data, &flags);

            if (n == 0) {
                break;
            }

            /* ERR_error_string_n() requires at least one byte */

            if (p >= last - 1) {
                goto next;
            }

            *p++ = ' ';

            ERR_error_string_n(n, (char *) p, last - p);

            while (p < last && *p) {
                p++;
            }

            if (p < last && *data && (flags & ERR_TXT_STRING)) {
                *p++ = ':';
                p = qjs_cpystrn(p, (u_char *) data, last - p);
            }

        next:

            (void) ERR_get_error();
        }

        if (p < last) {
            *p++ = ')';
        }
    }

    JS_ThrowTypeError(cx, "%.*s", (int) (p - errstr), errstr);
}


static int
qjs_webcrypto_module_init(JSContext *cx, JSModuleDef *m)
{
    int      rc;
    JSValue  proto;

    proto = JS_NewObject(cx);
    if (JS_IsException(proto)) {
        return -1;
    }

    JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_export,
                               njs_nitems(qjs_webcrypto_export));

    rc = JS_SetModuleExport(cx, m, "default", proto);
    if (rc != 0) {
        return -1;
    }

    return JS_SetModuleExportList(cx, m, qjs_webcrypto_export,
                                  njs_nitems(qjs_webcrypto_export));
}


static JSModuleDef *
qjs_webcrypto_init(JSContext *cx, const char *name)
{
    int          rc;
    JSValue      crypto, proto, global_obj;
    JSModuleDef  *m;

    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
                              QJS_CORE_CLASS_ID_WEBCRYPTO_KEY))
    {
        if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_WEBCRYPTO_KEY,
                        &qjs_webcrypto_key_class))
        {
            return NULL;
        }

        proto = JS_NewObject(cx);
        if (JS_IsException(proto)) {
            return NULL;
        }

        JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_key_proto,
                                   njs_nitems(qjs_webcrypto_key_proto));

        JS_SetClassProto(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY, proto);
    }

    global_obj = JS_GetGlobalObject(cx);

    crypto = JS_NewObject(cx);
    JS_SetPropertyFunctionList(cx, crypto, qjs_webcrypto_export,
                               njs_nitems(qjs_webcrypto_export));

    rc = JS_SetPropertyStr(cx, global_obj, "crypto", crypto);
    if (rc == -1) {
        return NULL;
    }

    JS_FreeValue(cx, global_obj);

    m = JS_NewCModule(cx, name, qjs_webcrypto_module_init);
    if (m == NULL) {
        return NULL;
    }

    if (JS_AddModuleExport(cx, m, "default") < 0) {
        return NULL;
    }

    rc = JS_AddModuleExportList(cx, m, qjs_webcrypto_export,
                                njs_nitems(qjs_webcrypto_export));
    if (rc != 0) {
        return NULL;
    }

    return m;
}
