From f42b2683d1ff6e11a388e3e6c0b8ec373caf5033 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 2 Apr 2026 22:32:24 -0700 Subject: [PATCH] WebCrypto: added AES-KW algorithm support. Supports 128, 192, and 256-bit key sizes with generateKey, importKey, and exportKey operations in raw and JWK formats. Also fixed deriveKey to accept 192-bit AES key lengths. --- auto/openssl | 11 + external/njs_webcrypto_module.c | 662 ++++++++++++++++++++++++-------- external/qjs_webcrypto_module.c | 602 +++++++++++++++++++++++------ test/webcrypto/aes_kw.t.mjs | 222 +++++++++++ test/webcrypto/wrap.t.mjs | 189 +++++++++ ts/njs_webcrypto.d.ts | 15 +- 6 files changed, 1427 insertions(+), 274 deletions(-) create mode 100644 test/webcrypto/aes_kw.t.mjs create mode 100644 test/webcrypto/wrap.t.mjs diff --git a/auto/openssl b/auto/openssl index 0dbaf5c1..bedd711b 100644 --- a/auto/openssl +++ b/auto/openssl @@ -33,6 +33,17 @@ if [ $NJS_OPENSSL = YES ]; then if [ $njs_found = yes ]; then + njs_feature="EVP_aes_128_wrap()" + njs_feature_name=NJS_HAVE_AES_WRAP + njs_feature_run= + njs_feature_test="#include + + int main() { + (void) EVP_aes_128_wrap(); + return 0; + }" + . auto/feature + njs_feature="OpenSSL version" njs_feature_name=NJS_OPENSSL_VERSION njs_feature_run=value diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index cccfd479..1b67b2fe 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -40,6 +40,7 @@ typedef enum { NJS_ALGORITHM_AES_GCM, NJS_ALGORITHM_AES_CTR, NJS_ALGORITHM_AES_CBC, + NJS_ALGORITHM_AES_KW, NJS_ALGORITHM_ECDSA, NJS_ALGORITHM_ECDH, NJS_ALGORITHM_PBKDF2, @@ -111,6 +112,10 @@ static njs_int_t njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data, static njs_int_t njs_cipher_aes_cbc(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt, njs_value_t *retval); +#if (NJS_HAVE_AES_WRAP) +static njs_int_t njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data, + njs_webcrypto_key_t *key, njs_bool_t encrypt, njs_value_t *retval); +#endif static njs_int_t njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t derive_key, njs_value_t *retval); static njs_int_t njs_ext_digest(njs_vm_t *vm, njs_value_t *args, @@ -123,6 +128,12 @@ static njs_int_t njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t verify, njs_value_t *retval); +static njs_int_t njs_webcrypto_export_key_raw(njs_vm_t *vm, + njs_webcrypto_key_t *key, njs_webcrypto_key_format_t fmt, + njs_value_t *retval); +static njs_int_t njs_webcrypto_cipher_core(njs_vm_t *vm, njs_str_t *data, + njs_webcrypto_key_t *key, njs_value_t *options, + njs_webcrypto_algorithm_t *alg, njs_bool_t encrypt, njs_value_t *retval); static njs_int_t njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t njs_ext_wrap_key(njs_vm_t *vm, njs_value_t *args, @@ -264,6 +275,19 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = { 1) }, +#if (NJS_HAVE_AES_WRAP) + { + njs_str("AES-KW"), + njs_webcrypto_algorithm(NJS_ALGORITHM_AES_KW, + NJS_KEY_USAGE_WRAP_KEY | + NJS_KEY_USAGE_UNWRAP_KEY | + NJS_KEY_USAGE_GENERATE_KEY, + NJS_KEY_FORMAT_RAW | + NJS_KEY_FORMAT_JWK, + 1) + }, +#endif + { njs_str("ECDSA"), njs_webcrypto_algorithm(NJS_ALGORITHM_ECDSA, @@ -406,7 +430,7 @@ static njs_str_t }, }; -static njs_str_t njs_webcrypto_alg_aes_name[3][3 + 1] = { +static njs_str_t njs_webcrypto_alg_aes_name[4][3 + 1] = { { njs_str("A128GCM"), njs_str("A192GCM"), @@ -427,6 +451,13 @@ static njs_str_t njs_webcrypto_alg_aes_name[3][3 + 1] = { njs_str("A256CBC"), njs_null_str, }, + + { + njs_str("A128KW"), + njs_str("A192KW"), + njs_str("A256KW"), + njs_null_str, + }, }; @@ -734,6 +765,11 @@ njs_ext_cipher(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } mask = encrypt ? NJS_KEY_USAGE_ENCRYPT : NJS_KEY_USAGE_DECRYPT; + + if (alg->type == NJS_ALGORITHM_AES_KW) { + mask = encrypt ? NJS_KEY_USAGE_WRAP_KEY : NJS_KEY_USAGE_UNWRAP_KEY; + } + if (njs_slow_path(!(key->usage & mask))) { njs_vm_type_error(vm, "provide key does not support %s operation", encrypt ? "encrypt" : "decrypt"); @@ -753,26 +789,8 @@ njs_ext_cipher(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - switch (alg->type) { - case NJS_ALGORITHM_RSA_OAEP: - ret = njs_cipher_pkey(vm, &data, key, encrypt, njs_value_arg(&result)); - break; - - case NJS_ALGORITHM_AES_GCM: - ret = njs_cipher_aes_gcm(vm, &data, key, options, encrypt, - njs_value_arg(&result)); - break; - - case NJS_ALGORITHM_AES_CTR: - ret = njs_cipher_aes_ctr(vm, &data, key, options, encrypt, - njs_value_arg(&result)); - break; - - case NJS_ALGORITHM_AES_CBC: - default: - ret = njs_cipher_aes_cbc(vm, &data, key, options, encrypt, - njs_value_arg(&result)); - } + ret = njs_webcrypto_cipher_core(vm, &data, key, options, alg, encrypt, + njs_value_arg(&result)); return njs_webcrypto_result(vm, &result, ret, retval); @@ -1455,6 +1473,102 @@ fail: } +#if (NJS_HAVE_AES_WRAP) +static njs_int_t +njs_cipher_aes_kw(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key, + njs_bool_t encrypt, njs_value_t *retval) +{ + int olen, dstlen; + u_char *dst; + njs_int_t ret; + EVP_CIPHER_CTX *ctx; + const EVP_CIPHER *cipher; + + if (encrypt) { + if (data->length < 16) { + njs_vm_type_error(vm, "AES-KW data must be at least 16 bytes"); + return NJS_ERROR; + } + + if (data->length % 8 != 0) { + njs_vm_type_error(vm, "AES-KW data must be a multiple of 8 bytes"); + return NJS_ERROR; + } + + } else { + if (data->length < 24) { + njs_vm_type_error(vm, "AES-KW data must be at least 24 bytes"); + return NJS_ERROR; + } + + if (data->length % 8 != 0) { + njs_vm_type_error(vm, "AES-KW data must be a multiple of 8 bytes"); + return NJS_ERROR; + } + } + + 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: + njs_vm_type_error(vm, "AES-KW Invalid key length"); + return NJS_ERROR; + } + + ctx = EVP_CIPHER_CTX_new(); + if (njs_slow_path(ctx == NULL)) { + njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed"); + return NJS_ERROR; + } + + EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); + + ret = EVP_CipherInit_ex(ctx, cipher, NULL, key->u.s.raw.start, NULL, + encrypt); + if (njs_slow_path(ret <= 0)) { + njs_webcrypto_error(vm, "EVP_%sInit_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = NJS_ERROR; + goto fail; + } + + dstlen = data->length + (encrypt ? 8 : 0); + dst = njs_mp_alloc(njs_vm_memory_pool(vm), dstlen); + if (njs_slow_path(dst == NULL)) { + njs_vm_memory_error(vm); + ret = NJS_ERROR; + goto fail; + } + + ret = EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length); + if (njs_slow_path(ret <= 0)) { + njs_webcrypto_error(vm, "EVP_%sUpdate() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = NJS_ERROR; + goto fail; + } + + ret = njs_vm_value_array_buffer_set(vm, retval, dst, olen); + +fail: + + EVP_CIPHER_CTX_free(ctx); + + return ret; +} +#endif + + static njs_int_t njs_ext_derive_ecdh(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t derive_key, njs_webcrypto_key_t *key, njs_value_t *retval) @@ -1723,10 +1837,12 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_AES_GCM: case NJS_ALGORITHM_AES_CTR: case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: - if (length != 16 && length != 32) { + if (length != 16 && length != 24 && length != 32) { njs_vm_type_error(vm, "deriveKey \"%V\" length must be " - "128 or 256", njs_algorithm_string(dalg)); + "128, 192, or 256", + njs_algorithm_string(dalg)); goto fail; } @@ -2430,11 +2546,8 @@ static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - BIO *bio; - BUF_MEM *mem; njs_int_t ret; njs_webcrypto_key_t *key; - PKCS8_PRIV_KEY_INFO *pkcs8; njs_opaque_value_t value; njs_webcrypto_key_format_t fmt; @@ -2462,136 +2575,9 @@ njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - switch (fmt) { - case NJS_KEY_FORMAT_JWK: - switch (key->alg->type) { - case NJS_ALGORITHM_RSASSA_PKCS1_v1_5: - case NJS_ALGORITHM_RSA_PSS: - case NJS_ALGORITHM_RSA_OAEP: - case NJS_ALGORITHM_ECDSA: - case NJS_ALGORITHM_ECDH: - ret = njs_export_jwk_asymmetric(vm, key, njs_value_arg(&value)); - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; - - case NJS_ALGORITHM_AES_GCM: - case NJS_ALGORITHM_AES_CTR: - case NJS_ALGORITHM_AES_CBC: - case NJS_ALGORITHM_HMAC: - ret = njs_export_jwk_oct(vm, key, njs_value_arg(&value)); - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; - - default: - break; - } - - break; - - case NJS_KEY_FORMAT_PKCS8: - if (!key->u.a.privat) { - njs_vm_type_error(vm, "public key of \"%V\" cannot be exported " - "as PKCS8", njs_algorithm_string(key->alg)); - goto fail; - } - - bio = BIO_new(BIO_s_mem()); - if (njs_slow_path(bio == NULL)) { - njs_webcrypto_error(vm, "BIO_new(BIO_s_mem()) failed"); - goto fail; - } - - njs_assert(key->u.a.pkey != NULL); - - pkcs8 = EVP_PKEY2PKCS8(key->u.a.pkey); - if (njs_slow_path(pkcs8 == NULL)) { - BIO_free(bio); - njs_webcrypto_error(vm, "EVP_PKEY2PKCS8() failed"); - goto fail; - } - - if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio, pkcs8)) { - BIO_free(bio); - PKCS8_PRIV_KEY_INFO_free(pkcs8); - njs_webcrypto_error(vm, "i2d_PKCS8_PRIV_KEY_INFO_bio() failed"); - goto fail; - } - - BIO_get_mem_ptr(bio, &mem); - - ret = njs_webcrypto_array_buffer(vm, njs_value_arg(&value), - (u_char *) mem->data, mem->length); - - BIO_free(bio); - PKCS8_PRIV_KEY_INFO_free(pkcs8); - - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; - - case NJS_KEY_FORMAT_SPKI: - if (key->u.a.privat) { - njs_vm_type_error(vm, "private key of \"%V\" cannot be exported " - "as SPKI", njs_algorithm_string(key->alg)); - goto fail; - } - - bio = BIO_new(BIO_s_mem()); - if (njs_slow_path(bio == NULL)) { - njs_webcrypto_error(vm, "BIO_new(BIO_s_mem()) failed"); - goto fail; - } - - njs_assert(key->u.a.pkey != NULL); - - if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) { - BIO_free(bio); - njs_webcrypto_error(vm, "i2d_PUBKEY_bio() failed"); - goto fail; - } - - BIO_get_mem_ptr(bio, &mem); - - ret = njs_webcrypto_array_buffer(vm, njs_value_arg(&value), - (u_char *) mem->data, mem->length); - - BIO_free(bio); - - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; - - case NJS_KEY_FORMAT_RAW: - default: - if (key->alg->type == NJS_ALGORITHM_ECDSA - || key->alg->type == NJS_ALGORITHM_ECDH) - { - ret = njs_export_raw_ec(vm, key, njs_value_arg(&value)); - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; - } - - ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&value), - key->u.s.raw.start, - key->u.s.raw.length); - if (njs_slow_path(ret != NJS_OK)) { - goto fail; - } - - break; + ret = njs_webcrypto_export_key_raw(vm, key, fmt, njs_value_arg(&value)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; } return njs_webcrypto_result(vm, &value, NJS_OK, retval); @@ -2838,6 +2824,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_AES_GCM: case NJS_ALGORITHM_AES_CTR: case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: case NJS_ALGORITHM_HMAC: if (alg->type == NJS_ALGORITHM_HMAC) { @@ -3857,6 +3844,7 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_AES_GCM: case NJS_ALGORITHM_AES_CTR: case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: if (fmt == NJS_KEY_FORMAT_RAW) { switch (key_data.length) { case 16: @@ -4375,20 +4363,371 @@ fail: static njs_int_t -njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, - njs_index_t unused, njs_value_t *retval) +njs_webcrypto_export_key_raw(njs_vm_t *vm, njs_webcrypto_key_t *key, + njs_webcrypto_key_format_t fmt, njs_value_t *retval) { - njs_vm_internal_error(vm, "\"unwrapKey\" not implemented"); + BIO *bio; + BUF_MEM *mem; + njs_int_t ret; + PKCS8_PRIV_KEY_INFO *pkcs8; + + switch (fmt) { + case NJS_KEY_FORMAT_JWK: + switch (key->alg->type) { + case NJS_ALGORITHM_RSASSA_PKCS1_v1_5: + case NJS_ALGORITHM_RSA_PSS: + case NJS_ALGORITHM_RSA_OAEP: + case NJS_ALGORITHM_ECDSA: + case NJS_ALGORITHM_ECDH: + return njs_export_jwk_asymmetric(vm, key, retval); + + case NJS_ALGORITHM_AES_GCM: + case NJS_ALGORITHM_AES_CTR: + case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: + case NJS_ALGORITHM_HMAC: + return njs_export_jwk_oct(vm, key, retval); + + default: + break; + } + + break; + + case NJS_KEY_FORMAT_PKCS8: + if (!key->u.a.privat) { + njs_vm_type_error(vm, "public key of \"%V\" cannot be exported " + "as PKCS8", njs_algorithm_string(key->alg)); + return NJS_ERROR; + } + + bio = BIO_new(BIO_s_mem()); + if (njs_slow_path(bio == NULL)) { + njs_webcrypto_error(vm, "BIO_new(BIO_s_mem()) failed"); + return NJS_ERROR; + } + + njs_assert(key->u.a.pkey != NULL); + + pkcs8 = EVP_PKEY2PKCS8(key->u.a.pkey); + if (njs_slow_path(pkcs8 == NULL)) { + BIO_free(bio); + njs_webcrypto_error(vm, "EVP_PKEY2PKCS8() failed"); + return NJS_ERROR; + } + + if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio, pkcs8)) { + BIO_free(bio); + PKCS8_PRIV_KEY_INFO_free(pkcs8); + njs_webcrypto_error(vm, + "i2d_PKCS8_PRIV_KEY_INFO_bio() failed"); + return NJS_ERROR; + } + + BIO_get_mem_ptr(bio, &mem); + + ret = njs_webcrypto_array_buffer(vm, retval, (u_char *) mem->data, + mem->length); + BIO_free(bio); + PKCS8_PRIV_KEY_INFO_free(pkcs8); + + return ret; + + case NJS_KEY_FORMAT_SPKI: + if (key->u.a.privat) { + njs_vm_type_error(vm, "private key of \"%V\" cannot be exported " + "as SPKI", njs_algorithm_string(key->alg)); + return NJS_ERROR; + } + + bio = BIO_new(BIO_s_mem()); + if (njs_slow_path(bio == NULL)) { + njs_webcrypto_error(vm, "BIO_new(BIO_s_mem()) failed"); + return NJS_ERROR; + } + + njs_assert(key->u.a.pkey != NULL); + + if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) { + BIO_free(bio); + njs_webcrypto_error(vm, "i2d_PUBKEY_bio() failed"); + return NJS_ERROR; + } + + BIO_get_mem_ptr(bio, &mem); + + ret = njs_webcrypto_array_buffer(vm, retval, (u_char *) mem->data, + mem->length); + BIO_free(bio); + + return ret; + + case NJS_KEY_FORMAT_RAW: + default: + if (key->alg->type == NJS_ALGORITHM_ECDSA + || key->alg->type == NJS_ALGORITHM_ECDH) + { + return njs_export_raw_ec(vm, key, retval); + } + + return njs_vm_value_array_buffer_set(vm, retval, + key->u.s.raw.start, + key->u.s.raw.length); + } + return NJS_ERROR; } +static njs_int_t +njs_webcrypto_cipher_core(njs_vm_t *vm, njs_str_t *data, + njs_webcrypto_key_t *key, njs_value_t *options, + njs_webcrypto_algorithm_t *alg, njs_bool_t encrypt, njs_value_t *retval) +{ + switch (alg->type) { + case NJS_ALGORITHM_RSA_OAEP: + return njs_cipher_pkey(vm, data, key, encrypt, retval); + + case NJS_ALGORITHM_AES_GCM: + return njs_cipher_aes_gcm(vm, data, key, options, encrypt, retval); + + case NJS_ALGORITHM_AES_CTR: + return njs_cipher_aes_ctr(vm, data, key, options, encrypt, retval); + + case NJS_ALGORITHM_AES_CBC: + return njs_cipher_aes_cbc(vm, data, key, options, encrypt, retval); + +#if (NJS_HAVE_AES_WRAP) + case NJS_ALGORITHM_AES_KW: + return njs_cipher_aes_kw(vm, data, key, encrypt, retval); +#endif + + default: + njs_vm_type_error(vm, "not implemented"); + return NJS_ERROR; + } +} + + static njs_int_t njs_ext_wrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - njs_vm_internal_error(vm, "\"wrapKey\" not implemented"); - return NJS_ERROR; + njs_int_t ret; + njs_str_t data; + njs_value_t *options; + njs_opaque_value_t exported, result; + njs_webcrypto_key_t *key, *wrapping_key; + njs_webcrypto_algorithm_t *alg; + njs_webcrypto_key_format_t fmt; + + fmt = njs_key_format(vm, njs_arg(args, nargs, 1)); + if (njs_slow_path(fmt == NJS_KEY_FORMAT_UNKNOWN)) { + goto fail; + } + + key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, + njs_arg(args, nargs, 2)); + if (njs_slow_path(key == NULL)) { + njs_vm_type_error(vm, "\"key\" is not a CryptoKey object"); + goto fail; + } + + wrapping_key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, + njs_arg(args, nargs, 3)); + if (njs_slow_path(wrapping_key == NULL)) { + njs_vm_type_error(vm, "\"wrappingKey\" is not a CryptoKey object"); + goto fail; + } + + options = njs_arg(args, nargs, 4); + alg = njs_key_algorithm(vm, options); + if (njs_slow_path(alg == NULL)) { + goto fail; + } + + if (njs_slow_path(!key->extractable)) { + njs_vm_type_error(vm, "provided key cannot be extracted"); + goto fail; + } + + if (njs_slow_path(!(wrapping_key->usage & NJS_KEY_USAGE_WRAP_KEY))) { + njs_vm_type_error(vm, "wrapping key does not support wrapKey"); + goto fail; + } + + if (njs_slow_path(wrapping_key->alg != alg)) { + njs_vm_type_error(vm, "cannot wrap using \"%V\" with \"%V\" key", + njs_algorithm_string(wrapping_key->alg), + njs_algorithm_string(alg)); + goto fail; + } + + ret = njs_webcrypto_export_key_raw(vm, key, fmt, + njs_value_arg(&exported)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + if (fmt == NJS_KEY_FORMAT_JWK) { + njs_opaque_value_t json_str; + + ret = njs_vm_json_stringify(vm, njs_value_arg(&exported), 1, + njs_value_arg(&json_str)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + njs_value_assign(&exported, &json_str); + } + + ret = njs_vm_value_to_bytes(vm, &data, njs_value_arg(&exported)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + ret = njs_webcrypto_cipher_core(vm, &data, wrapping_key, options, + alg, 1, njs_value_arg(&result)); + + return njs_webcrypto_result(vm, &result, ret, retval); + +fail: + + return njs_webcrypto_result(vm, NULL, NJS_ERROR, retval); +} + + +static njs_int_t +njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused, njs_value_t *retval) +{ + unsigned usage; + njs_int_t ret; + njs_str_t data, key_data; + njs_value_t *options; + njs_bool_t extractable; + njs_opaque_value_t decrypted, value; + njs_webcrypto_key_t *wrapping_key, *ikey; + njs_webcrypto_algorithm_t *alg, *key_alg; + njs_webcrypto_key_format_t fmt; + + fmt = njs_key_format(vm, njs_arg(args, nargs, 1)); + if (njs_slow_path(fmt == NJS_KEY_FORMAT_UNKNOWN)) { + goto fail; + } + + ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 2)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + wrapping_key = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, + njs_arg(args, nargs, 3)); + if (njs_slow_path(wrapping_key == NULL)) { + njs_vm_type_error(vm, "\"unwrappingKey\" is not a CryptoKey object"); + goto fail; + } + + options = njs_arg(args, nargs, 4); + alg = njs_key_algorithm(vm, options); + if (njs_slow_path(alg == NULL)) { + goto fail; + } + + key_alg = njs_key_algorithm(vm, njs_arg(args, nargs, 5)); + if (njs_slow_path(key_alg == NULL)) { + goto fail; + } + + extractable = njs_value_bool(njs_arg(args, nargs, 6)); + + ret = njs_key_usage(vm, njs_arg(args, nargs, 7), &usage); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + if (njs_slow_path(!(wrapping_key->usage & NJS_KEY_USAGE_UNWRAP_KEY))) { + njs_vm_type_error(vm, "unwrapping key does not support unwrapKey"); + goto fail; + } + + if (njs_slow_path(wrapping_key->alg != alg)) { + njs_vm_type_error(vm, "cannot unwrap using \"%V\" with \"%V\" key", + njs_algorithm_string(wrapping_key->alg), + njs_algorithm_string(alg)); + goto fail; + } + + ret = njs_webcrypto_cipher_core(vm, &data, wrapping_key, options, + alg, 0, njs_value_arg(&decrypted)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + ret = njs_vm_value_to_bytes(vm, &key_data, njs_value_arg(&decrypted)); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + if (njs_slow_path(usage & ~key_alg->usage)) { + njs_vm_type_error(vm, "unsupported key usage for \"%V\" key", + njs_algorithm_string(key_alg)); + goto fail; + } + + if (njs_slow_path(!(fmt & key_alg->fmt))) { + njs_vm_type_error(vm, "unsupported key fmt for \"%V\" key", + njs_algorithm_string(key_alg)); + goto fail; + } + + ikey = njs_webcrypto_key_alloc(vm, key_alg, usage, extractable); + if (njs_slow_path(ikey == NULL)) { + goto fail; + } + + if (fmt == NJS_KEY_FORMAT_RAW) { + switch (key_alg->type) { + case NJS_ALGORITHM_AES_GCM: + case NJS_ALGORITHM_AES_CTR: + case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: + switch (key_data.length) { + case 16: + case 24: + case 32: + break; + default: + njs_vm_type_error(vm, "AES Invalid key length"); + goto fail; + } + + ikey->u.s.raw = key_data; + break; + + case NJS_ALGORITHM_HMAC: + default: + ikey->u.s.raw = key_data; + break; + } + + } else { + njs_vm_type_error(vm, "unwrapKey: unsupported format \"%V\"", + njs_format_string(fmt)); + goto fail; + } + + ret = njs_vm_external_create(vm, njs_value_arg(&value), + njs_webcrypto_crypto_key_proto_id, ikey, 0); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + return njs_webcrypto_result(vm, &value, NJS_OK, retval); + +fail: + + return njs_webcrypto_result(vm, NULL, NJS_ERROR, retval); } @@ -4493,6 +4832,7 @@ njs_key_ext_algorithm(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, case NJS_ALGORITHM_AES_GCM: case NJS_ALGORITHM_AES_CTR: case NJS_ALGORITHM_AES_CBC: + case NJS_ALGORITHM_AES_KW: /* AesKeyGenParams */ njs_value_number_set(njs_value_arg(&val), key->u.s.raw.length * 8); diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index 179af59a..443e6851 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -48,6 +48,7 @@ typedef enum { QJS_ALGORITHM_AES_GCM, QJS_ALGORITHM_AES_CTR, QJS_ALGORITHM_AES_CBC, + QJS_ALGORITHM_AES_KW, QJS_ALGORITHM_ECDSA, QJS_ALGORITHM_ECDH, QJS_ALGORITHM_PBKDF2, @@ -115,6 +116,10 @@ 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, @@ -129,6 +134,10 @@ static JSValue qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); 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); @@ -137,6 +146,11 @@ static JSValue qjs_webcrypto_key_extractable(JSContext *cx, 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, @@ -258,6 +272,19 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = { 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, @@ -408,7 +435,7 @@ static njs_str_t }, }; -static njs_str_t qjs_webcrypto_alg_aes_name[3][3 + 1] = { +static njs_str_t qjs_webcrypto_alg_aes_name[4][3 + 1] = { { njs_str("A128GCM"), njs_str("A192GCM"), @@ -429,6 +456,13 @@ static njs_str_t qjs_webcrypto_alg_aes_name[3][3 + 1] = { njs_str("A256CBC"), njs_null_str, }, + + { + njs_str("A128KW"), + njs_str("A192KW"), + njs_str("A256KW"), + njs_null_str, + }, }; @@ -442,7 +476,9 @@ static const JSCFunctionListEntry qjs_webcrypto_subtle[] = { 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), }; @@ -501,6 +537,11 @@ qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val, } 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"); @@ -520,37 +561,9 @@ qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val, return ret; } - switch (alg->type) { - case QJS_ALGORITHM_RSA_OAEP: - ret = qjs_cipher_pkey(cx, &data, key, encrypt); - if (JS_IsException(ret)) { - goto fail; - } - - break; - - case QJS_ALGORITHM_AES_GCM: - ret = qjs_cipher_aes_gcm(cx, &data, key, options, encrypt); - if (JS_IsException(ret)) { - goto fail; - } - - break; - - case QJS_ALGORITHM_AES_CTR: - ret = qjs_cipher_aes_ctr(cx, &data, key, options, encrypt); - if (JS_IsException(ret)) { - goto fail; - } - - break; - - case QJS_ALGORITHM_AES_CBC: - default: - ret = qjs_cipher_aes_cbc(cx, &data, key, options, encrypt); - if (JS_IsException(ret)) { - goto fail; - } + ret = qjs_webcrypto_cipher_core(cx, &data, key, options, alg, encrypt); + if (JS_IsException(ret)) { + goto fail; } return qjs_promise_result(cx, ret); @@ -1244,6 +1257,102 @@ fail: } +#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) { @@ -1942,10 +2051,12 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, case QJS_ALGORITHM_AES_GCM: case QJS_ALGORITHM_AES_CTR: case QJS_ALGORITHM_AES_CBC: + case QJS_ALGORITHM_AES_KW: - if (length != 16 && length != 32) { + if (length != 16 && length != 24 && length != 32) { JS_ThrowTypeError(cx, "deriveKey \"%s\" length must be " - "128 or 256", qjs_algorithm_string(dalg)); + "128, 192, or 256", + qjs_algorithm_string(dalg)); return JS_EXCEPTION; } @@ -2208,35 +2319,13 @@ qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, int argc, static JSValue -qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, - JSValueConst *argv) +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; - qjs_webcrypto_key_t *key; - PKCS8_PRIV_KEY_INFO *pkcs8; - 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; - } + BIO *bio; + BUF_MEM *mem; + JSValue ret; + PKCS8_PRIV_KEY_INFO *pkcs8; switch (fmt) { case QJS_KEY_FORMAT_JWK: @@ -2246,43 +2335,32 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_ALGORITHM_RSA_OAEP: case QJS_ALGORITHM_ECDSA: case QJS_ALGORITHM_ECDH: - ret = qjs_export_jwk_asymmetric(cx, key); - if (JS_IsException(ret)) { - goto fail; - } - - break; + return qjs_export_jwk_asymmetric(cx, key); case QJS_ALGORITHM_AES_GCM: case QJS_ALGORITHM_AES_CTR: case QJS_ALGORITHM_AES_CBC: + case QJS_ALGORITHM_AES_KW: case QJS_ALGORITHM_HMAC: - ret = qjs_export_jwk_oct(cx, key); - if (JS_IsException(ret)) { - goto fail; - } - - break; + 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)); - goto fail; + return JS_EXCEPTION; } - break; - 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)); - goto fail; + return JS_EXCEPTION; } bio = BIO_new(BIO_s_mem()); if (bio == NULL) { qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed"); - goto fail; + return JS_EXCEPTION; } njs_assert(key->u.a.pkey != NULL); @@ -2291,41 +2369,37 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, if (pkcs8 == NULL) { BIO_free(bio); qjs_webcrypto_error(cx, "EVP_PKEY2PKCS8() failed"); - goto fail; + 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"); - goto fail; + 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); - if (JS_IsException(ret)) { - goto fail; - } - - break; + 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)); - goto fail; + return JS_EXCEPTION; } bio = BIO_new(BIO_s_mem()); if (bio == NULL) { qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed"); - goto fail; + return JS_EXCEPTION; } njs_assert(key->u.a.pkey != NULL); @@ -2333,48 +2407,95 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) { BIO_free(bio); qjs_webcrypto_error(cx, "i2d_PUBKEY_bio() failed"); - goto fail; + return JS_EXCEPTION; } BIO_get_mem_ptr(bio, &mem); ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data, mem->length); - BIO_free(bio); - if (JS_IsException(ret)) { - goto fail; - } - - break; + return ret; case QJS_KEY_FORMAT_RAW: default: if (key->alg->type == QJS_ALGORITHM_ECDSA || key->alg->type == QJS_ALGORITHM_ECDH) { - ret = qjs_export_raw_ec(cx, key); - if (JS_IsException(ret)) { - goto fail; - } - - break; - } else { - ret = JS_NewArrayBufferCopy(cx, key->u.s.raw.start, - key->u.s.raw.length); - if (JS_IsException(ret)) { - goto fail; - } + return qjs_export_raw_ec(cx, key); } + return JS_NewArrayBufferCopy(cx, key->u.s.raw.start, + key->u.s.raw.length); } +} - return qjs_promise_result(cx, ret); -fail: +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); - return qjs_promise_result(cx, JS_EXCEPTION); + 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); } @@ -2593,6 +2714,7 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, 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); @@ -3703,6 +3825,7 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, 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 (key_data.length) { case 16: @@ -4227,6 +4350,260 @@ fail: } +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 mask, usage; + JSValue options, ret, decrypted, key_value; + njs_str_t data, key_data; + qjs_webcrypto_key_t *wrapping_key, *ikey; + 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; + } + + mask = QJS_KEY_USAGE_UNWRAP_KEY; + if (!(wrapping_key->usage & mask)) { + 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; + } + + ret = qjs_typed_array_data(cx, decrypted, &key_data); + if (JS_IsException(ret)) { + JS_FreeValue(cx, decrypted); + goto fail; + } + + if (usage & ~key_alg->usage) { + JS_FreeValue(cx, decrypted); + JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key", + qjs_algorithm_string(key_alg)); + goto fail; + } + + if (!(fmt & key_alg->fmt)) { + JS_FreeValue(cx, decrypted); + JS_ThrowTypeError(cx, "unsupported key fmt for \"%s\" key", + qjs_algorithm_string(key_alg)); + goto fail; + } + + key_value = qjs_webcrypto_key_make(cx, key_alg, usage, + JS_ToBool(cx, argv[5])); + if (JS_IsException(key_value)) { + JS_FreeValue(cx, decrypted); + goto fail; + } + + ikey = JS_GetOpaque2(cx, key_value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + if (fmt == QJS_KEY_FORMAT_RAW) { + switch (key_alg->type) { + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + case QJS_ALGORITHM_AES_KW: + switch (key_data.length) { + case 16: + case 24: + case 32: + break; + default: + JS_FreeValue(cx, decrypted); + JS_FreeValue(cx, key_value); + JS_ThrowTypeError(cx, "AES Invalid key length"); + goto fail; + } + + ikey->u.s.raw.start = js_malloc(cx, key_data.length); + if (ikey->u.s.raw.start == NULL) { + JS_FreeValue(cx, decrypted); + JS_FreeValue(cx, key_value); + JS_ThrowOutOfMemory(cx); + goto fail; + } + + ikey->u.s.raw.length = key_data.length; + memcpy(ikey->u.s.raw.start, key_data.start, key_data.length); + break; + + case QJS_ALGORITHM_HMAC: + default: + ikey->u.s.raw.start = js_malloc(cx, key_data.length); + if (ikey->u.s.raw.start == NULL) { + JS_FreeValue(cx, decrypted); + JS_FreeValue(cx, key_value); + JS_ThrowOutOfMemory(cx); + goto fail; + } + + ikey->u.s.raw.length = key_data.length; + memcpy(ikey->u.s.raw.start, key_data.start, key_data.length); + break; + } + + } else { + JS_FreeValue(cx, decrypted); + JS_FreeValue(cx, key_value); + JS_ThrowTypeError(cx, "unwrapKey: unsupported format"); + goto fail; + } + + JS_FreeValue(cx, decrypted); + + 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) { @@ -4339,6 +4716,7 @@ qjs_webcrypto_key_algorithm(JSContext *cx, JSValueConst this_val) 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", diff --git a/test/webcrypto/aes_kw.t.mjs b/test/webcrypto/aes_kw.t.mjs new file mode 100644 index 00000000..ea10a8e7 --- /dev/null +++ b/test/webcrypto/aes_kw.t.mjs @@ -0,0 +1,222 @@ +/*--- +includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js] +flags: [async] +---*/ + +async function generate_kek(usages) { + return await crypto.subtle.generateKey( + {name: "AES-KW", length: 128}, true, usages); +} + + +async function test(params) { + try { + await crypto.subtle.generateKey( + {name: "AES-KW", length: 128}, true, ["wrapKey", "unwrapKey"]); + } catch (e) { + if (e.message.indexOf("AES-KW") !== -1) { + return 'SKIPPED'; + } + + throw e; + } + + let key; + + if (params.generate) { + key = await crypto.subtle.generateKey( + {name: "AES-KW", length: params.generate}, + true, ["wrapKey", "unwrapKey"]); + + let raw = await crypto.subtle.exportKey("raw", key); + if (raw.byteLength !== params.generate / 8) { + throw Error(`generateKey length mismatch: ${raw.byteLength}`); + } + + return 'SUCCESS'; + } + + let kek; + + if (params.jwk_import) { + kek = await crypto.subtle.importKey("jwk", params.jwk_import, + {name: "AES-KW"}, false, ["wrapKey", "unwrapKey"]); + + } else { + let extractable = params.jwk_roundtrip ? true : false; + + kek = await crypto.subtle.importKey("raw", + Buffer.from(params.kek, "hex"), + {name: "AES-KW"}, extractable, ["wrapKey", "unwrapKey"]); + } + + if (params.jwk_roundtrip) { + let jwk = await crypto.subtle.exportKey("jwk", kek); + if (jwk.kty !== "oct") { + throw Error(`JWK kty mismatch: ${jwk.kty}`); + } + + return 'SUCCESS'; + } + + let data = Buffer.from(params.data, "hex"); + let expected = Buffer.from(params.expected, "hex"); + + /* import the plaintext as an AES key to wrap */ + let inner = await crypto.subtle.importKey("raw", data, + {name: "AES-GCM"}, true, ["encrypt"]); + + /* wrap */ + let wrapped = await crypto.subtle.wrapKey("raw", inner, kek, + {name: "AES-KW"}); + wrapped = Buffer.from(wrapped); + + if (wrapped.compare(expected) != 0) { + throw Error(`AES-KW wrap failed: ${wrapped.toString("hex")}` + + ` != ${params.expected}`); + } + + /* unwrap */ + let unwrapped = await crypto.subtle.unwrapKey("raw", expected, kek, + {name: "AES-KW"}, {name: "AES-GCM"}, true, ["encrypt"]); + + let raw = await crypto.subtle.exportKey("raw", unwrapped); + if (Buffer.from(raw).compare(data) != 0) { + throw Error(`AES-KW unwrap failed`); + } + + return 'SUCCESS'; +} + +async function test_wrap_short_data() { + let kek = await generate_kek(["wrapKey", "unwrapKey"]); + let key = await crypto.subtle.importKey("raw", + Buffer.from("0011223344556677", "hex"), + {name: "HMAC", hash: "SHA-256"}, true, ["sign"]); + + await crypto.subtle.wrapKey("raw", key, kek, {name: "AES-KW"}); +} + + +async function test_wrap_non_multiple() { + let kek = await generate_kek(["wrapKey", "unwrapKey"]); + let key = await crypto.subtle.importKey("raw", + Buffer.from("00112233445566778899AABBCCDDEEFF00112233", "hex"), + {name: "HMAC", hash: "SHA-256"}, true, ["sign"]); + + await crypto.subtle.wrapKey("raw", key, kek, {name: "AES-KW"}); +} + + +async function test_unwrap_short_data() { + let kek = await generate_kek(["wrapKey", "unwrapKey"]); + + await crypto.subtle.unwrapKey("raw", + crypto.getRandomValues(new Uint8Array(23)), + kek, {name: "AES-KW"}, {name: "AES-GCM"}, true, ["encrypt"]); +} + + +async function test_unwrap_non_multiple() { + let kek = await generate_kek(["wrapKey", "unwrapKey"]); + + await crypto.subtle.unwrapKey("raw", + crypto.getRandomValues(new Uint8Array(25)), + kek, {name: "AES-KW"}, {name: "AES-GCM"}, true, ["encrypt"]); +} + + +async function test_no_wrapkey_usage() { + let kek = await generate_kek(["unwrapKey"]); + let key = await crypto.subtle.importKey("raw", + Buffer.from("00112233445566778899AABBCCDDEEFF", "hex"), + {name: "AES-GCM"}, true, ["encrypt"]); + + await crypto.subtle.wrapKey("raw", key, kek, {name: "AES-KW"}); +} + + +async function test_no_unwrapkey_usage() { + let kek = await generate_kek(["wrapKey"]); + let wrapped = Buffer.from( + "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5", + "hex"); + + await crypto.subtle.unwrapKey("raw", wrapped, kek, + {name: "AES-KW"}, {name: "AES-GCM"}, true, ["encrypt"]); +} + + +let aes_kw_tsuite = { + name: "AES-KW wrap/unwrap", + skip: () => (!has_buffer() || !has_webcrypto()), + T: test, + prepare_args: (args) => args, + + tests: [ + /* generateKey */ + { generate: 128 }, + { generate: 192 }, + { generate: 256 }, + + /* RFC 3394 4.1: 128-bit KEK, 128-bit data */ + { kek: "000102030405060708090A0B0C0D0E0F", + data: "00112233445566778899AABBCCDDEEFF", + expected: "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5" }, + + /* RFC 3394 4.3: 192-bit KEK, 128-bit data */ + { kek: "000102030405060708090A0B0C0D0E0F1011121314151617", + data: "00112233445566778899AABBCCDDEEFF", + expected: "96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D" }, + + /* RFC 3394 4.5: 256-bit KEK, 128-bit data */ + { kek: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + data: "00112233445566778899AABBCCDDEEFF", + expected: "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7" }, + + /* RFC 3394 4.6: 256-bit KEK, 256-bit data */ + { kek: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + data: "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", + expected: "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21" }, + + /* JWK roundtrip: export + reimport */ + { kek: "000102030405060708090A0B0C0D0E0F", + jwk_roundtrip: true }, + + /* JWK import 128-bit */ + { jwk_import: { kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", + alg: "A128KW", key_ops: ["wrapKey", "unwrapKey"] }, + data: "00112233445566778899AABBCCDDEEFF", + expected: "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5" }, + + /* JWK import 256-bit */ + { jwk_import: { kty: "oct", + k: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", + alg: "A256KW", key_ops: ["wrapKey", "unwrapKey"] }, + data: "00112233445566778899AABBCCDDEEFF", + expected: "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7" }, +]}; + +let aes_kw_error_tsuite = { + name: "AES-KW errors", + skip: () => (!has_buffer() || !has_webcrypto()), + T: (params) => params.T(), + prepare_args: (args) => args, + + tests: [ + { T: test_wrap_short_data, + exception: "TypeError: AES-KW data must be at least 16 bytes" }, + { T: test_wrap_non_multiple, + exception: "TypeError: AES-KW data must be a multiple of 8 bytes" }, + { T: test_unwrap_short_data, + exception: "TypeError: AES-KW data must be at least 24 bytes" }, + { T: test_unwrap_non_multiple, + exception: "TypeError: AES-KW data must be a multiple of 8 bytes" }, + { T: test_no_wrapkey_usage, + exception: "TypeError: wrapping key does not support wrapKey" }, + { T: test_no_unwrapkey_usage, + exception: "TypeError: unwrapping key does not support unwrapKey" }, +]}; + +run([aes_kw_tsuite, aes_kw_error_tsuite]) +.then($DONE, $DONE); diff --git a/test/webcrypto/wrap.t.mjs b/test/webcrypto/wrap.t.mjs new file mode 100644 index 00000000..9a5b6b0f --- /dev/null +++ b/test/webcrypto/wrap.t.mjs @@ -0,0 +1,189 @@ +/*--- +includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCryptoUtils.js] +flags: [async] +---*/ + +async function test(params) { + let wrapping_key; + + try { + wrapping_key = await crypto.subtle.generateKey( + params.wrap_alg, true, ["wrapKey", "unwrapKey"]); + } catch (e) { + if (e.message.indexOf("unknown algorithm") !== -1 + || e.message.indexOf("Unrecognized algorithm") !== -1) + { + return 'SKIPPED'; + } + + throw e; + } + + let key = await crypto.subtle.generateKey( + params.key_alg, true, params.key_usage); + + let wrapped = await crypto.subtle.wrapKey( + params.format, key, wrapping_key, params.wrap_params); + + let unwrapped = await crypto.subtle.unwrapKey( + params.format, wrapped, wrapping_key, params.wrap_params, + params.key_alg, true, params.key_usage); + + /* verify the unwrapped key works */ + if (params.verify_encrypt) { + let data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16]); + let enc = await crypto.subtle.encrypt( + params.verify_encrypt, unwrapped, data); + let dec = await crypto.subtle.decrypt( + params.verify_encrypt, unwrapped, enc); + + if (Buffer.from(dec).compare(Buffer.from(data)) != 0) { + throw Error("unwrapped key encrypt/decrypt roundtrip failed"); + } + } + + if (params.verify_sign) { + let data = new Uint8Array([1, 2, 3, 4]); + let sig = await crypto.subtle.sign( + params.verify_sign, unwrapped, data); + + if (sig.byteLength === 0) { + throw Error("unwrapped key sign produced empty signature"); + } + } + + /* verify raw key material matches */ + if (params.format === "raw" && params.check_raw) { + let orig = await crypto.subtle.exportKey("raw", key); + let unwrapped_raw = await crypto.subtle.exportKey("raw", unwrapped); + if (Buffer.from(orig).compare(Buffer.from(unwrapped_raw)) != 0) { + throw Error("unwrapped key raw material mismatch"); + } + } + + return 'SUCCESS'; +} + +async function test_non_extractable() { + let wrapping_key = await crypto.subtle.generateKey( + {name: "AES-KW", length: 256}, false, + ["wrapKey", "unwrapKey"]); + let key = await crypto.subtle.generateKey( + {name: "AES-GCM", length: 128}, false, + ["encrypt", "decrypt"]); + + await crypto.subtle.wrapKey("raw", key, wrapping_key, + {name: "AES-KW"}); +} + +async function test_no_wrapKey_usage() { + let wrapping_key = await crypto.subtle.importKey("raw", + crypto.getRandomValues(new Uint8Array(16)), + {name: "AES-KW"}, false, ["unwrapKey"]); + let key = await crypto.subtle.generateKey( + {name: "AES-GCM", length: 128}, true, + ["encrypt", "decrypt"]); + + await crypto.subtle.wrapKey("raw", key, wrapping_key, + {name: "AES-KW"}); +} + +async function test_no_unwrapKey_usage() { + let wrapping_key = await crypto.subtle.importKey("raw", + crypto.getRandomValues(new Uint8Array(16)), + {name: "AES-KW"}, false, ["wrapKey"]); + + let data = crypto.getRandomValues(new Uint8Array(24)); + await crypto.subtle.unwrapKey("raw", data, wrapping_key, + {name: "AES-KW"}, {name: "AES-GCM"}, true, + ["encrypt", "decrypt"]); +} + +let wrap_tsuite = { + name: "wrapKey/unwrapKey", + skip: () => (!has_buffer() || !has_webcrypto()), + T: test, + prepare_args: (args) => args, + + tests: [ + /* AES-KW wrapping an AES-GCM key (raw format) */ + { wrap_alg: {name: "AES-KW", length: 256}, + key_alg: {name: "AES-GCM", length: 128}, + key_usage: ["encrypt", "decrypt"], + format: "raw", + wrap_params: {name: "AES-KW"}, + check_raw: true, + verify_encrypt: {name: "AES-GCM", + iv: crypto.getRandomValues(new Uint8Array(12))} }, + + /* AES-KW wrapping an AES-CBC key */ + { wrap_alg: {name: "AES-KW", length: 128}, + key_alg: {name: "AES-CBC", length: 256}, + key_usage: ["encrypt", "decrypt"], + format: "raw", + wrap_params: {name: "AES-KW"}, + check_raw: true }, + + /* AES-KW wrapping an HMAC key */ + { wrap_alg: {name: "AES-KW", length: 256}, + key_alg: {name: "HMAC", hash: "SHA-256"}, + key_usage: ["sign", "verify"], + format: "raw", + wrap_params: {name: "AES-KW"}, + check_raw: true, + verify_sign: {name: "HMAC"} }, + + /* AES-GCM wrapping an AES key */ + { wrap_alg: {name: "AES-GCM", length: 256}, + key_alg: {name: "AES-GCM", length: 128}, + key_usage: ["encrypt", "decrypt"], + format: "raw", + wrap_params: {name: "AES-GCM", + iv: crypto.getRandomValues(new Uint8Array(12))}, + check_raw: true }, + + /* AES-CBC wrapping an AES key */ + { wrap_alg: {name: "AES-CBC", length: 256}, + key_alg: {name: "AES-CBC", length: 128}, + key_usage: ["encrypt", "decrypt"], + format: "raw", + wrap_params: {name: "AES-CBC", + iv: crypto.getRandomValues(new Uint8Array(16))}, + check_raw: true }, + + /* AES-GCM wrapping an HMAC key (covers HMAC unwrap path) */ + { wrap_alg: {name: "AES-GCM", length: 256}, + key_alg: {name: "HMAC", hash: "SHA-256"}, + key_usage: ["sign", "verify"], + format: "raw", + wrap_params: {name: "AES-GCM", + iv: crypto.getRandomValues(new Uint8Array(12))}, + check_raw: true, + verify_sign: {name: "HMAC"} }, + + /* AES-CTR wrapping an AES key */ + { wrap_alg: {name: "AES-CTR", length: 256}, + key_alg: {name: "AES-GCM", length: 128}, + key_usage: ["encrypt", "decrypt"], + format: "raw", + wrap_params: {name: "AES-CTR", + counter: crypto.getRandomValues(new Uint8Array(16)), + length: 64}, + check_raw: true }, +]}; + +let wrap_error_tsuite = { + name: "wrapKey/unwrapKey errors", + skip: () => (!has_buffer() || !has_webcrypto()), + T: (params) => params.T(), + prepare_args: (args) => args, + + tests: [ + { T: test_non_extractable, exception: true }, + { T: test_no_wrapKey_usage, exception: true }, + { T: test_no_unwrapKey_usage, exception: true }, +]}; + +run([wrap_tsuite, wrap_error_tsuite]) +.then($DONE, $DONE); diff --git a/ts/njs_webcrypto.d.ts b/ts/njs_webcrypto.d.ts index 9aa9c307..f9bdbc98 100644 --- a/ts/njs_webcrypto.d.ts +++ b/ts/njs_webcrypto.d.ts @@ -55,7 +55,7 @@ interface HmacImportParams { hash: HashVariants; } -type AesVariants = "AES-CTR" | "AES-CBC" | "AES-GCM"; +type AesVariants = "AES-CTR" | "AES-CBC" | "AES-GCM" | "AES-KW"; interface AesImportParams { name: AesVariants; @@ -323,6 +323,19 @@ interface SubtleCrypto { key: CryptoKey, signature: NjsStringOrBuffer, data: NjsStringOrBuffer): Promise; + + wrapKey(format: "raw" | "pkcs8" | "spki" | "jwk", + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: CipherAlgorithm | "AES-KW"): Promise; + + unwrapKey(format: "raw" | "pkcs8" | "spki" | "jwk", + wrappedKey: NjsStringOrBuffer, + unwrappingKey: CryptoKey, + unwrapAlgorithm: CipherAlgorithm | "AES-KW", + unwrappedKeyAlgorithm: ImportAlgorithm, + extractable: boolean, + keyUsages: Array): Promise; } interface Crypto { -- 2.47.3