Introduced WebCrypto API according to W3C spec.
The following methods were implemented:
crypto.getRandomValues()
crypto.subtle.importKey()
format: raw, pkcs8, spki
algorithm: AES-CBC, AES-CTR, AES-GCM,
ECDSA, HKDF, HMAC, PBKDF2,
RSASSA-PKCS1-v1_5, RSA-OAEP, RSA-PSS
crypto.subtle.decrypt()
crypto.subtle.encrypt()
algorithm: AES-CBC, AES-CTR, AES-GCM,
RSA-OAEP
crypto.subtle.deriveBits()
crypto.subtle.deriveKey()
algorithm: HKDF, PBKDF2
crypto.subtle.digest()
algorithm: SHA-1, SHA-256, SHA-384, SHA-512
crypto.subtle.sign()
crypto.subtle.verify()
algorithm: ECDSA, HMAC, RSASSA-PKCS1-v1_5, RSA-PSS
diff --git a/auto/make b/auto/make
index b66864e..e5ba7bf 100644
--- a/auto/make
+++ b/auto/make
@@ -75,7 +75,7 @@
$NJS_BUILD_DIR/njs: \\
$NJS_BUILD_DIR/libnjs.a \\
- src/njs_shell.c
+ src/njs_shell.c external/njs_webcrypto.h external/njs_webcrypto.c
\$(NJS_LINK) -o $NJS_BUILD_DIR/njs \$(NJS_CFLAGS) \\
$NJS_LIB_AUX_CFLAGS \$(NJS_LIB_INCS) -Injs \\
src/njs_shell.c \\
@@ -159,7 +159,8 @@
cat << END >> $NJS_MAKEFILE
-$NJS_BUILD_DIR/$njs_externals_obj: $njs_src
+$NJS_BUILD_DIR/$njs_externals_obj: \\
+ $njs_src external/njs_webcrypto.h external/njs_webcrypto.c
\$(NJS_CC) -c \$(NJS_CFLAGS) $NJS_LIB_AUX_CFLAGS \\
\$(NJS_LIB_INCS) -Injs \\
-o $NJS_BUILD_DIR/$njs_externals_obj \\
diff --git a/auto/openssl b/auto/openssl
new file mode 100644
index 0000000..1140c6f
--- /dev/null
+++ b/auto/openssl
@@ -0,0 +1,56 @@
+
+# Copyright (C) Dmitry Volyntsev
+# Copyright (C) NGINX, Inc.
+
+
+NJS_OPENSSL_LIB=
+NJS_HAVE_OPENSSL=NO
+
+
+njs_found=no
+
+
+njs_feature="OpenSSL library"
+njs_feature_name=NJS_HAVE_OPENSSL
+njs_feature_run=yes
+njs_feature_incs=
+njs_feature_libs="-lcrypto"
+njs_feature_test="#include <openssl/evp.h>
+
+ int main() {
+ OpenSSL_add_all_algorithms();
+ return 0;
+ }"
+. auto/feature
+
+
+if [ $njs_found = yes ]; then
+ njs_feature="OpenSSL HKDF"
+ njs_feature_name=NJS_HAVE_OPENSSL_HKDF
+ njs_feature_test="#include <openssl/evp.h>
+ #include <openssl/kdf.h>
+
+ int main(void) {
+ EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+
+ EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha256());
+ EVP_PKEY_CTX_free(pctx);
+
+ return 0;
+ }"
+ . auto/feature
+
+ njs_feature="OpenSSL EVP_MD_CTX_new()"
+ njs_feature_name=NJS_HAVE_OPENSSL_EVP_MD_CTX_NEW
+ njs_feature_test="#include <openssl/evp.h>
+
+ int main(void) {
+ EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+ EVP_MD_CTX_free(ctx);
+ return 0;
+ }"
+ . auto/feature
+
+ NJS_HAVE_OPENSSL=YES
+ NJS_OPENSSL_LIB="$njs_feature_libs"
+fi
diff --git a/auto/sources b/auto/sources
index 4e5e65c..9544e09 100644
--- a/auto/sources
+++ b/auto/sources
@@ -2,6 +2,7 @@
src/njs_diyfp.c \
src/njs_dtoa.c \
src/njs_dtoa_fixed.c \
+ src/njs_str.c \
src/njs_strtod.c \
src/njs_murmur_hash.c \
src/njs_djb_hash.c \
diff --git a/auto/summary b/auto/summary
index 9399ed9..90bac3d 100644
--- a/auto/summary
+++ b/auto/summary
@@ -15,6 +15,10 @@
echo " + using readline library: $NJS_READLINE_LIB"
fi
+if [ $NJS_HAVE_OPENSSL = YES ]; then
+ echo " + using OpenSSL library: $NJS_OPENSSL_LIB"
+fi
+
echo
echo " njs build dir: $NJS_BUILD_DIR"
diff --git a/configure b/configure
index f3e845d..9e84823 100755
--- a/configure
+++ b/configure
@@ -26,12 +26,13 @@
. auto/explicit_bzero
. auto/pcre
. auto/readline
+. auto/openssl
. auto/sources
NJS_LIB_AUX_CFLAGS="$NJS_PCRE_CFLAGS"
NJS_LIBS="$NJS_LIBRT"
-NJS_LIB_AUX_LIBS="$NJS_PCRE_LIB"
+NJS_LIB_AUX_LIBS="$NJS_PCRE_LIB $NJS_OPENSSL_LIB"
. auto/make
diff --git a/external/njs_webcrypto.c b/external/njs_webcrypto.c
new file mode 100644
index 0000000..00f9e63
--- /dev/null
+++ b/external/njs_webcrypto.c
@@ -0,0 +1,2666 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <njs_main.h>
+#include "njs_webcrypto.h"
+
+#include <openssl/bn.h>
+#include <openssl/bio.h>
+#include <openssl/x509.h>
+#include <openssl/evp.h>
+#include <openssl/aes.h>
+#include <openssl/rsa.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/crypto.h>
+
+#if NJS_HAVE_OPENSSL_HKDF
+#include <openssl/kdf.h>
+#endif
+
+#if NJS_HAVE_OPENSSL_EVP_MD_CTX_NEW
+#define njs_evp_md_ctx_new() EVP_MD_CTX_new();
+#define njs_evp_md_ctx_free(_ctx) EVP_MD_CTX_free(_ctx);
+#else
+#define njs_evp_md_ctx_new() EVP_MD_CTX_create();
+#define njs_evp_md_ctx_free(_ctx) EVP_MD_CTX_destroy(_ctx);
+#endif
+
+
+typedef enum {
+ NJS_KEY_FORMAT_RAW = 1 << 1,
+ NJS_KEY_FORMAT_PKCS8 = 1 << 2,
+ NJS_KEY_FORMAT_SPKI = 1 << 3,
+ NJS_KEY_FORMAT_JWK = 1 << 4,
+ NJS_KEY_FORMAT_UNKNOWN = 1 << 5,
+} njs_webcrypto_key_format_t;
+
+
+typedef enum {
+ NJS_KEY_USAGE_DECRYPT = 1 << 1,
+ NJS_KEY_USAGE_DERIVE_BITS = 1 << 2,
+ NJS_KEY_USAGE_DERIVE_KEY = 1 << 3,
+ NJS_KEY_USAGE_ENCRYPT = 1 << 4,
+ NJS_KEY_USAGE_GENERATE_KEY = 1 << 5,
+ NJS_KEY_USAGE_SIGN = 1 << 6,
+ NJS_KEY_USAGE_VERIFY = 1 << 7,
+ NJS_KEY_USAGE_WRAP_KEY = 1 << 8,
+ NJS_KEY_USAGE_UNSUPPORTED = 1 << 9,
+ NJS_KEY_USAGE_UNWRAP_KEY = 1 << 10,
+} njs_webcrypto_key_usage_t;
+
+
+typedef enum {
+ NJS_ALGORITHM_RSA_OAEP,
+ NJS_ALGORITHM_AES_GCM,
+ NJS_ALGORITHM_AES_CTR,
+ NJS_ALGORITHM_AES_CBC,
+ NJS_ALGORITHM_RSASSA_PKCS1_v1_5,
+ NJS_ALGORITHM_RSA_PSS,
+ NJS_ALGORITHM_ECDSA,
+ NJS_ALGORITHM_ECDH,
+ NJS_ALGORITHM_PBKDF2,
+ NJS_ALGORITHM_HKDF,
+ NJS_ALGORITHM_HMAC,
+} njs_webcrypto_alg_t;
+
+
+typedef enum {
+ NJS_HASH_SHA1,
+ NJS_HASH_SHA256,
+ NJS_HASH_SHA384,
+ NJS_HASH_SHA512,
+} njs_webcrypto_hash_t;
+
+
+typedef enum {
+ NJS_CURVE_P256,
+ NJS_CURVE_P384,
+ NJS_CURVE_P521,
+} njs_webcrypto_curve_t;
+
+
+typedef struct {
+ njs_str_t name;
+ uintptr_t value;
+} njs_webcrypto_entry_t;
+
+
+typedef struct {
+ njs_webcrypto_alg_t type;
+ unsigned usage;
+ unsigned fmt;
+} njs_webcrypto_algorithm_t;
+
+
+typedef struct {
+ njs_webcrypto_algorithm_t *alg;
+ unsigned usage;
+ njs_webcrypto_hash_t hash;
+ njs_webcrypto_curve_t curve;
+
+ EVP_PKEY *pkey;
+ njs_str_t raw;
+} njs_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 njs_int_t njs_ext_cipher(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_cipher_pkey(njs_vm_t *vm, njs_str_t *data,
+ njs_webcrypto_key_t *key, njs_index_t encrypt);
+static njs_int_t njs_cipher_aes_gcm(njs_vm_t *vm, njs_str_t *data,
+ njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt);
+static njs_int_t njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data,
+ njs_webcrypto_key_t *key, njs_value_t *options, njs_bool_t encrypt);
+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);
+static njs_int_t njs_ext_derive(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t derive_key);
+static njs_int_t njs_ext_digest(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_import_key(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_sign(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t verify);
+static njs_int_t njs_ext_unwrap_key(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_wrap_key(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_ext_get_random_values(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused);
+
+static void njs_webcrypto_cleanup_pkey(void *data);
+static njs_webcrypto_key_format_t njs_key_format(njs_vm_t *vm,
+ njs_value_t *value, njs_str_t *format);
+static njs_int_t njs_key_usage(njs_vm_t *vm, njs_value_t *value,
+ unsigned *mask);
+static njs_webcrypto_algorithm_t *njs_key_algorithm(njs_vm_t *vm,
+ njs_value_t *value);
+static njs_str_t *njs_algorithm_string(njs_webcrypto_algorithm_t *algorithm);
+static njs_int_t njs_algorithm_hash(njs_vm_t *vm, njs_value_t *value,
+ njs_webcrypto_hash_t *hash);
+static const EVP_MD *njs_algorithm_hash_digest(njs_webcrypto_hash_t hash);
+static njs_int_t njs_algorithm_curve(njs_vm_t *vm, njs_value_t *value,
+ njs_webcrypto_curve_t *curve);
+
+static njs_int_t njs_webcrypto_result(njs_vm_t *vm, njs_value_t *result,
+ njs_int_t rc);
+static void njs_webcrypto_error(njs_vm_t *vm, const char *fmt, ...);
+
+static njs_webcrypto_entry_t njs_webcrypto_alg[] = {
+
+#define njs_webcrypto_algorithm(type, usage_mask, fmt_mask) \
+ (uintptr_t) & (njs_webcrypto_algorithm_t) { type, usage_mask, fmt_mask }
+
+ {
+ njs_str("RSA-OAEP"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_RSA_OAEP,
+ NJS_KEY_USAGE_ENCRYPT |
+ NJS_KEY_USAGE_DECRYPT |
+ NJS_KEY_USAGE_WRAP_KEY |
+ NJS_KEY_USAGE_UNWRAP_KEY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_PKCS8 |
+ NJS_KEY_FORMAT_SPKI)
+ },
+
+ {
+ njs_str("AES-GCM"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_AES_GCM,
+ NJS_KEY_USAGE_ENCRYPT |
+ NJS_KEY_USAGE_DECRYPT |
+ NJS_KEY_USAGE_WRAP_KEY |
+ NJS_KEY_USAGE_UNWRAP_KEY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_str("AES-CTR"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_AES_CTR,
+ NJS_KEY_USAGE_ENCRYPT |
+ NJS_KEY_USAGE_DECRYPT |
+ NJS_KEY_USAGE_WRAP_KEY |
+ NJS_KEY_USAGE_UNWRAP_KEY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_str("AES-CBC"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_AES_CBC,
+ NJS_KEY_USAGE_ENCRYPT |
+ NJS_KEY_USAGE_DECRYPT |
+ NJS_KEY_USAGE_WRAP_KEY |
+ NJS_KEY_USAGE_UNWRAP_KEY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_str("RSASSA-PKCS1-v1_5"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_RSASSA_PKCS1_v1_5,
+ NJS_KEY_USAGE_SIGN |
+ NJS_KEY_USAGE_VERIFY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_PKCS8 |
+ NJS_KEY_FORMAT_SPKI)
+ },
+
+ {
+ njs_str("RSA-PSS"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_RSA_PSS,
+ NJS_KEY_USAGE_SIGN |
+ NJS_KEY_USAGE_VERIFY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_PKCS8 |
+ NJS_KEY_FORMAT_SPKI)
+ },
+
+ {
+ njs_str("ECDSA"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_ECDSA,
+ NJS_KEY_USAGE_SIGN |
+ NJS_KEY_USAGE_VERIFY |
+ NJS_KEY_USAGE_GENERATE_KEY,
+ NJS_KEY_FORMAT_PKCS8 |
+ NJS_KEY_FORMAT_SPKI)
+ },
+
+ {
+ njs_str("ECDH"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_ECDH,
+ NJS_KEY_USAGE_DERIVE_KEY |
+ NJS_KEY_USAGE_DERIVE_BITS |
+ NJS_KEY_USAGE_GENERATE_KEY |
+ NJS_KEY_USAGE_UNSUPPORTED,
+ NJS_KEY_FORMAT_UNKNOWN)
+ },
+
+ {
+ njs_str("PBKDF2"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_PBKDF2,
+ NJS_KEY_USAGE_DERIVE_KEY |
+ NJS_KEY_USAGE_DERIVE_BITS,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_str("HKDF"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_HKDF,
+ NJS_KEY_USAGE_DERIVE_KEY |
+ NJS_KEY_USAGE_DERIVE_BITS,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_str("HMAC"),
+ njs_webcrypto_algorithm(NJS_ALGORITHM_HMAC,
+ NJS_KEY_USAGE_GENERATE_KEY |
+ NJS_KEY_USAGE_SIGN |
+ NJS_KEY_USAGE_VERIFY,
+ NJS_KEY_FORMAT_RAW)
+ },
+
+ {
+ njs_null_str,
+ 0
+ }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_hash[] = {
+ { njs_str("SHA-256"), NJS_HASH_SHA256 },
+ { njs_str("SHA-384"), NJS_HASH_SHA384 },
+ { njs_str("SHA-512"), NJS_HASH_SHA512 },
+ { njs_str("SHA-1"), NJS_HASH_SHA1 },
+ { njs_null_str, 0 }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_curve[] = {
+ { njs_str("P-256"), NJS_CURVE_P256 },
+ { njs_str("P-384"), NJS_CURVE_P384 },
+ { njs_str("P-521"), NJS_CURVE_P521 },
+ { njs_null_str, 0 }
+};
+
+
+static njs_webcrypto_entry_t njs_webcrypto_usage[] = {
+ { njs_str("decrypt"), NJS_KEY_USAGE_DECRYPT },
+ { njs_str("deriveBits"), NJS_KEY_USAGE_DERIVE_BITS },
+ { njs_str("deriveKey"), NJS_KEY_USAGE_DERIVE_KEY },
+ { njs_str("encrypt"), NJS_KEY_USAGE_ENCRYPT },
+ { njs_str("sign"), NJS_KEY_USAGE_SIGN },
+ { njs_str("unwrapKey"), NJS_KEY_USAGE_UNWRAP_KEY },
+ { njs_str("verify"), NJS_KEY_USAGE_VERIFY },
+ { njs_str("wrapKey"), NJS_KEY_USAGE_WRAP_KEY },
+ { njs_null_str, 0 }
+};
+
+
+static njs_external_t njs_ext_webcrypto_crypto_key[] = {
+
+ {
+ .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+ .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+ .u.property = {
+ .value = "CryptoKey",
+ }
+ },
+};
+
+
+static njs_external_t njs_ext_subtle_webcrypto[] = {
+
+ {
+ .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+ .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+ .u.property = {
+ .value = "SubtleCrypto",
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("decrypt"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_cipher,
+ .magic8 = 0,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("deriveBits"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_derive,
+ .magic8 = 0,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("deriveKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_derive,
+ .magic8 = 1,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("digest"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_digest,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("encrypt"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_cipher,
+ .magic8 = 1,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("exportKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_export_key,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("generateKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_generate_key,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("importKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_import_key,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("sign"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_sign,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("unwrapKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_unwrap_key,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("verify"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_sign,
+ .magic8 = 1,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("wrapKey"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_wrap_key,
+ }
+ },
+
+};
+
+static njs_external_t njs_ext_webcrypto[] = {
+
+ {
+ .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+ .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+ .u.property = {
+ .value = "Crypto",
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("getRandomValues"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_ext_get_random_values,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_OBJECT,
+ .name.string = njs_str("subtle"),
+ .enumerable = 1,
+ .writable = 1,
+ .u.object = {
+ .enumerable = 1,
+ .properties = njs_ext_subtle_webcrypto,
+ .nproperties = njs_nitems(njs_ext_subtle_webcrypto),
+ }
+ },
+
+};
+
+
+static njs_int_t njs_webcrypto_crypto_key_proto_id;
+
+
+static njs_int_t
+njs_ext_cipher(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t encrypt)
+{
+ unsigned mask;
+ njs_int_t ret;
+ njs_str_t data;
+ njs_value_t *options;
+ njs_webcrypto_key_t *key;
+ njs_webcrypto_algorithm_t *alg;
+
+ options = njs_arg(args, nargs, 1);
+ alg = njs_key_algorithm(vm, options);
+ if (njs_slow_path(alg == NULL)) {
+ 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_type_error(vm, "\"key\" is not a CryptoKey object");
+ goto fail;
+ }
+
+ mask = encrypt ? NJS_KEY_USAGE_ENCRYPT : NJS_KEY_USAGE_DECRYPT;
+ if (njs_slow_path(!(key->usage & mask))) {
+ njs_type_error(vm, "provide key does not support %s operation",
+ encrypt ? "encrypt" : "decrypt");
+ goto fail;
+ }
+
+ if (njs_slow_path(key->alg != alg)) {
+ njs_type_error(vm, "cannot %s using \"%V\" with \"%V\" key",
+ encrypt ? "encrypt" : "decrypt",
+ njs_algorithm_string(key->alg),
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 3));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ switch (alg->type) {
+ case NJS_ALGORITHM_RSA_OAEP:
+ ret = njs_cipher_pkey(vm, &data, key, encrypt);
+ break;
+
+ case NJS_ALGORITHM_AES_GCM:
+ ret = njs_cipher_aes_gcm(vm, &data, key, options, encrypt);
+ break;
+
+ case NJS_ALGORITHM_AES_CTR:
+ ret = njs_cipher_aes_ctr(vm, &data, key, options, encrypt);
+ break;
+
+ case NJS_ALGORITHM_AES_CBC:
+ default:
+ ret = njs_cipher_aes_cbc(vm, &data, key, options, encrypt);
+ }
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), ret);
+
+fail:
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_cipher_pkey(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+ njs_index_t encrypt)
+{
+ u_char *dst;
+ size_t outlen;
+ njs_int_t 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->pkey, NULL);
+ if (njs_slow_path(ctx == NULL)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed");
+ return NJS_ERROR;
+ }
+
+ if (encrypt) {
+ init = EVP_PKEY_encrypt_init;
+ cipher = EVP_PKEY_encrypt;
+
+ } else {
+ init = EVP_PKEY_decrypt_init;
+ cipher = EVP_PKEY_decrypt;
+ }
+
+ ret = init(ctx);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_%scrypt_init() failed",
+ encrypt ? "en" : "de");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ md = njs_algorithm_hash_digest(key->hash);
+
+ EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
+ EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md);
+ EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md);
+
+ ret = cipher(ctx, NULL, &outlen, data->start, data->length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_%scrypt() failed",
+ encrypt ? "en" : "de");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ dst = njs_mp_alloc(njs_vm_memory_pool(vm), outlen);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = cipher(ctx, dst, &outlen, data->start, data->length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_%scrypt() failed",
+ encrypt ? "en" : "de");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, outlen);
+
+fail:
+
+ EVP_PKEY_CTX_free(ctx);
+
+ return ret;
+}
+
+
+static njs_int_t
+njs_cipher_aes_gcm(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+ njs_value_t *options, njs_bool_t encrypt)
+{
+ int len, outlen, dstlen;
+ u_char *dst, *p;
+ int64_t taglen;
+ njs_str_t iv, aad;
+ njs_int_t ret;
+ njs_value_t value;
+ EVP_CIPHER_CTX *ctx;
+ const EVP_CIPHER *cipher;
+
+ static const njs_value_t string_iv = njs_string("iv");
+ static const njs_value_t string_ad = njs_string("additionalData");
+ static const njs_value_t string_tl = njs_string("tagLength");
+
+ switch (key->raw.length) {
+ case 16:
+ cipher = EVP_aes_128_gcm();
+ break;
+
+ case 32:
+ cipher = EVP_aes_256_gcm();
+ break;
+
+ default:
+ njs_type_error(vm, "AES-GCM Invalid key length");
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_iv), &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "AES-GCM algorithm.iv is not provided");
+ }
+
+ return NJS_ERROR;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &iv, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ taglen = 128;
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_tl), &value);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ return NJS_ERROR;
+ }
+
+ if (njs_is_defined(&value)) {
+ ret = njs_value_to_integer(vm, &value, &taglen);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+ }
+
+ if (njs_slow_path(taglen != 32
+ && taglen != 64
+ && taglen != 96
+ && taglen != 104
+ && taglen != 112
+ && taglen != 120
+ && taglen != 128))
+ {
+ njs_type_error(vm, "AES-GCM Invalid tagLength");
+ return NJS_ERROR;
+ }
+
+ taglen /= 8;
+
+ if (njs_slow_path(!encrypt && (data->length < (size_t) taglen))) {
+ njs_type_error(vm, "AES-GCM data is too short");
+ 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;
+ }
+
+ ret = EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.length, NULL);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = EVP_CipherInit_ex(ctx, NULL, NULL, key->raw.start, iv.start,
+ encrypt);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ if (!encrypt) {
+ ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen,
+ &data->start[data->length - taglen]);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+ }
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_ad), &value);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ return NJS_ERROR;
+ }
+
+ aad.length = 0;
+
+ if (njs_is_defined(&value)) {
+ ret = njs_vm_value_to_bytes(vm, &aad, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+ }
+
+ if (aad.length != 0) {
+ ret = EVP_CipherUpdate(ctx, NULL, &outlen, aad.start, aad.length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+ }
+
+ dstlen = data->length + EVP_CIPHER_CTX_block_size(ctx) + taglen;
+ dst = njs_mp_alloc(njs_vm_memory_pool(vm), dstlen);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ return NJS_ERROR;
+ }
+
+ ret = EVP_CipherUpdate(ctx, dst, &outlen, data->start,
+ data->length - (encrypt ? 0 : taglen));
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ p = &dst[outlen];
+ len = EVP_CIPHER_CTX_block_size(ctx);
+
+ ret = EVP_CipherFinal_ex(ctx, p, &len);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ outlen += len;
+ p += len;
+
+ if (encrypt) {
+ ret = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, p);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_CIPHER_CTX_ctrl() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ outlen += taglen;
+ }
+
+ ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, outlen);
+
+fail:
+
+ EVP_CIPHER_CTX_free(ctx);
+
+ return ret;
+}
+
+
+static njs_int_t
+njs_cipher_aes_ctr128(njs_vm_t *vm, const EVP_CIPHER *cipher, u_char *key,
+ u_char *data, size_t dlen, u_char *counter, u_char *dst, int *olen,
+ njs_bool_t encrypt)
+{
+ int len, outlen;
+ njs_int_t ret;
+ EVP_CIPHER_CTX *ctx;
+
+ ctx = EVP_CIPHER_CTX_new();
+ if (njs_slow_path(ctx == NULL)) {
+ njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
+ return NJS_ERROR;
+ }
+
+ ret = EVP_CipherInit_ex(ctx, cipher, NULL, key, counter, encrypt);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sInit_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = EVP_CipherUpdate(ctx, dst, &outlen, data, dlen);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sUpdate() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = EVP_CipherFinal_ex(ctx, &dst[outlen], &len);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ outlen += len;
+ *olen = outlen;
+
+ ret = NJS_OK;
+
+fail:
+
+ EVP_CIPHER_CTX_free(ctx);
+
+ return ret;
+}
+
+
+njs_inline njs_uint_t
+njs_ceil_div(njs_uint_t dend, njs_uint_t dsor)
+{
+ return (dsor == 0) ? 0 : 1 + (dend - 1) / dsor;
+}
+
+
+njs_inline BIGNUM *
+njs_bn_counter128(njs_str_t *ctr, njs_uint_t bits)
+{
+ njs_uint_t 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 = njs_ceil_div(bits, 8);
+
+ memcpy(buf, &ctr->start[ctr->length - bytes], bytes);
+
+ buf[0] &= ~(0xFF << remainder);
+
+ return BN_bin2bn(buf, bytes, NULL);
+}
+
+
+njs_inline void
+njs_counter128_reset(u_char *src, u_char *dst, njs_uint_t bits)
+{
+ size_t index;
+ njs_uint_t 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 njs_int_t
+njs_cipher_aes_ctr(njs_vm_t *vm, njs_str_t *data, njs_webcrypto_key_t *key,
+ njs_value_t *options, njs_bool_t encrypt)
+{
+ int len, len2;
+ u_char *dst;
+ int64_t length;
+ BIGNUM *total, *blocks, *left, *ctr;
+ njs_int_t ret;
+ njs_str_t iv;
+ njs_uint_t size1;
+ njs_value_t value;
+ const EVP_CIPHER *cipher;
+ u_char iv2[16];
+
+ static const njs_value_t string_counter = njs_string("counter");
+ static const njs_value_t string_length = njs_string("length");
+
+ switch (key->raw.length) {
+ case 16:
+ cipher = EVP_aes_128_ctr();
+ break;
+
+ case 32:
+ cipher = EVP_aes_256_ctr();
+ break;
+
+ default:
+ njs_type_error(vm, "AES-CTR Invalid key length");
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_counter),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "AES-CTR algorithm.counter is not provided");
+ }
+
+ return NJS_ERROR;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &iv, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ if (njs_slow_path(iv.length != 16)) {
+ njs_type_error(vm, "AES-CTR algorithm.counter must be 16 bytes long");
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_length),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "AES-CTR algorithm.length is not provided");
+ }
+
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_to_integer(vm, &value, &length);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ if (njs_slow_path(length == 0 || length > 128)) {
+ njs_type_error(vm, "AES-CTR algorithm.length "
+ "must be between 1 and 128");
+ return NJS_ERROR;
+ }
+
+ ctr = NULL;
+ blocks = NULL;
+ left = NULL;
+
+ total = BN_new();
+ if (njs_slow_path(total == NULL)) {
+ njs_webcrypto_error(vm, "BN_new() failed");
+ return NJS_ERROR;
+ }
+
+ ret = BN_lshift(total, BN_value_one(), length);
+ if (njs_slow_path(ret != 1)) {
+ njs_webcrypto_error(vm, "BN_lshift() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ctr = njs_bn_counter128(&iv, length);
+ if (njs_slow_path(ctr == NULL)) {
+ njs_webcrypto_error(vm, "BN_bin2bn() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ blocks = BN_new();
+ if (njs_slow_path(blocks == NULL)) {
+ njs_webcrypto_error(vm, "BN_new() failed");
+ return NJS_ERROR;
+ }
+
+ ret = BN_set_word(blocks, njs_ceil_div(data->length, AES_BLOCK_SIZE));
+ if (njs_slow_path(ret != 1)) {
+ njs_webcrypto_error(vm, "BN_set_word() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ ret = BN_cmp(blocks, total);
+ if (njs_slow_path(ret > 0)) {
+ njs_type_error(vm, "AES-CTR repeated counter");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ left = BN_new();
+ if (njs_slow_path(left == NULL)) {
+ njs_webcrypto_error(vm, "BN_new() failed");
+ return NJS_ERROR;
+ }
+
+ ret = BN_sub(left, total, ctr);
+ if (njs_slow_path(ret != 1)) {
+ njs_webcrypto_error(vm, "BN_sub() failed");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ dst = njs_mp_alloc(njs_vm_memory_pool(vm),
+ data->length + EVP_MAX_BLOCK_LENGTH);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ return NJS_ERROR;
+ }
+
+ ret = BN_cmp(left, blocks);
+ if (ret >= 0) {
+
+ /*
+ * Doing a single run if a counter is not wrapped-around
+ * during the ciphering.
+ * */
+
+ ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start,
+ data->start, data->length, iv.start, dst,
+ &len, encrypt);
+ if (njs_slow_path(ret != NJS_OK)) {
+ 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;
+
+ ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start, data->start, size1,
+ iv.start, dst, &len, encrypt);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ njs_counter128_reset(iv.start, (u_char *) iv2, length);
+
+ ret = njs_cipher_aes_ctr128(vm, cipher, key->raw.start, &data->start[size1],
+ data->length - size1, iv2, &dst[size1], &len2,
+ encrypt);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ len += len2;
+
+done:
+
+ ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), 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 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)
+{
+ int olen_max, olen, olen2;
+ u_char *dst;
+ unsigned remainder;
+ njs_str_t iv;
+ njs_int_t ret;
+ njs_value_t value;
+ EVP_CIPHER_CTX *ctx;
+ const EVP_CIPHER *cipher;
+
+ static const njs_value_t string_iv = njs_string("iv");
+
+ switch (key->raw.length) {
+ case 16:
+ cipher = EVP_aes_128_cbc();
+ break;
+
+ case 32:
+ cipher = EVP_aes_256_cbc();
+ break;
+
+ default:
+ njs_type_error(vm, "AES-CBC Invalid key length");
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_iv), &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "AES-CBC algorithm.iv is not provided");
+ }
+
+ return NJS_ERROR;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &iv, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ if (njs_slow_path(iv.length != 16)) {
+ njs_type_error(vm, "AES-CBC algorithm.iv must be 16 bytes long");
+ return NJS_ERROR;
+ }
+
+ 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 (njs_slow_path(ctx == NULL)) {
+ njs_webcrypto_error(vm, "EVP_CIPHER_CTX_new() failed");
+ return NJS_ERROR;
+ }
+
+ ret = EVP_CipherInit_ex(ctx, cipher, NULL, key->raw.start, iv.start,
+ encrypt);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%SInit_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ dst = njs_mp_alloc(njs_vm_memory_pool(vm), olen_max);
+ if (njs_slow_path(dst == NULL)) {
+ njs_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 = EVP_CipherFinal_ex(ctx, &dst[olen], &olen2);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_%sFinal_ex() failed",
+ encrypt ? "Encrypt" : "Decrypt");
+ ret = NJS_ERROR;
+ goto fail;
+ }
+
+ olen += olen2;
+
+ ret = njs_vm_value_array_buffer_set(vm, njs_vm_retval(vm), dst, olen);
+
+fail:
+
+ EVP_CIPHER_CTX_free(ctx);
+
+ return ret;
+}
+
+
+static njs_int_t
+njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t derive_key)
+{
+ u_char *k;
+ size_t olen;
+ int64_t iterations, length;
+ EVP_PKEY *pkey;
+ unsigned usage, mask;
+ njs_int_t ret;
+ njs_str_t salt, info;
+ njs_value_t value, *aobject, *dobject;
+ const EVP_MD *md;
+ EVP_PKEY_CTX *pctx;
+ njs_mp_cleanup_t *cln;
+ njs_webcrypto_key_t *key, *dkey;
+ njs_webcrypto_hash_t hash;
+ njs_webcrypto_algorithm_t *alg, *dalg;
+
+ static const njs_value_t string_info = njs_string("info");
+ static const njs_value_t string_salt = njs_string("salt");
+ static const njs_value_t string_length = njs_string("length");
+ static const njs_value_t string_iterations = njs_string("iterations");
+
+ aobject = njs_arg(args, nargs, 1);
+ alg = njs_key_algorithm(vm, aobject);
+ if (njs_slow_path(alg == NULL)) {
+ 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_type_error(vm, "\"baseKey\" is not a CryptoKey object");
+ goto fail;
+ }
+
+ mask = derive_key ? NJS_KEY_USAGE_DERIVE_KEY : NJS_KEY_USAGE_DERIVE_BITS;
+ if (njs_slow_path(!(key->usage & mask))) {
+ njs_type_error(vm, "provide key does not support \"%s\" operation",
+ derive_key ? "deriveKey" : "deriveBits");
+ goto fail;
+ }
+
+ if (njs_slow_path(key->alg != alg)) {
+ njs_type_error(vm, "cannot derive %s using \"%V\" with \"%V\" key",
+ derive_key ? "key" : "bits",
+ njs_algorithm_string(key->alg),
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ dobject = njs_arg(args, nargs, 3);
+
+ if (derive_key) {
+ dalg = njs_key_algorithm(vm, dobject);
+ if (njs_slow_path(dalg == NULL)) {
+ goto fail;
+ }
+
+ ret = njs_value_property(vm, dobject, njs_value_arg(&string_length),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "derivedKeyAlgorithm.length "
+ "is not provided");
+ goto fail;
+ }
+ }
+
+ } else {
+ dalg = NULL;
+ njs_value_assign(&value, dobject);
+ }
+
+ ret = njs_value_to_integer(vm, &value, &length);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ dkey = NULL;
+ length /= 8;
+
+ if (derive_key) {
+ switch (dalg->type) {
+ case NJS_ALGORITHM_AES_GCM:
+ case NJS_ALGORITHM_AES_CTR:
+ case NJS_ALGORITHM_AES_CBC:
+
+ if (length != 16 && length != 32) {
+ njs_type_error(vm, "deriveKey \"%V\" length must be 128 or 256",
+ njs_algorithm_string(dalg));
+ goto fail;
+ }
+
+ break;
+
+ default:
+ njs_internal_error(vm, "not implemented deriveKey: \"%V\"",
+ njs_algorithm_string(dalg));
+ goto fail;
+ }
+
+ ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ if (njs_slow_path(usage & ~dalg->usage)) {
+ njs_type_error(vm, "unsupported key usage for \"%V\" key",
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ dkey = njs_mp_zalloc(njs_vm_memory_pool(vm),
+ sizeof(njs_webcrypto_key_t));
+ if (njs_slow_path(dkey == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ dkey->alg = dalg;
+ dkey->usage = usage;
+ }
+
+ k = njs_mp_zalloc(njs_vm_memory_pool(vm), length);
+ if (njs_slow_path(k == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ switch (alg->type) {
+ case NJS_ALGORITHM_PBKDF2:
+ ret = njs_algorithm_hash(vm, aobject, &hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ ret = njs_value_property(vm, aobject, njs_value_arg(&string_salt),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "PBKDF2 algorithm.salt is not provided");
+ }
+
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &salt, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ if (njs_slow_path(salt.length < 16)) {
+ njs_type_error(vm, "PBKDF2 algorithm.salt must be "
+ "at least 16 bytes long");
+ goto fail;
+ }
+
+ ret = njs_value_property(vm, aobject, njs_value_arg(&string_iterations),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "PBKDF2 algorithm.iterations "
+ "is not provided");
+ }
+
+ goto fail;
+ }
+
+ ret = njs_value_to_integer(vm, &value, &iterations);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ md = njs_algorithm_hash_digest(hash);
+
+ ret = PKCS5_PBKDF2_HMAC((char *) key->raw.start, key->raw.length,
+ salt.start, salt.length, iterations, md,
+ length, k);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "PKCS5_PBKDF2_HMAC() failed");
+ goto fail;
+ }
+ break;
+
+ case NJS_ALGORITHM_HKDF:
+#ifdef NJS_HAVE_OPENSSL_HKDF
+ ret = njs_algorithm_hash(vm, aobject, &hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ ret = njs_value_property(vm, aobject, njs_value_arg(&string_salt),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "HKDF algorithm.salt is not provided");
+ }
+
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &salt, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ ret = njs_value_property(vm, aobject, njs_value_arg(&string_info),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "HKDF algorithm.info is not provided");
+ }
+
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &info, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+ if (njs_slow_path(pctx == NULL)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_new_id() failed");
+ goto fail;
+ }
+
+ ret = EVP_PKEY_derive_init(pctx);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_derive_init() failed");
+ goto free;
+ }
+
+ md = njs_algorithm_hash_digest(hash);
+
+ ret = EVP_PKEY_CTX_set_hkdf_md(pctx, md);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_hkdf_md() failed");
+ goto free;
+ }
+
+ ret = EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt.start, salt.length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_set1_hkdf_salt() failed");
+ goto free;
+ }
+
+ ret = EVP_PKEY_CTX_set1_hkdf_key(pctx, key->raw.start, key->raw.length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_set1_hkdf_key() failed");
+ goto free;
+ }
+
+ ret = EVP_PKEY_CTX_add1_hkdf_info(pctx, info.start, info.length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_add1_hkdf_info() failed");
+ goto free;
+ }
+
+ olen = (size_t) length;
+ ret = EVP_PKEY_derive(pctx, k, &olen);
+ if (njs_slow_path(ret <= 0 || olen != (size_t) length)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_derive() failed");
+ goto free;
+ }
+
+free:
+
+ EVP_PKEY_CTX_free(pctx);
+
+ if (njs_slow_path(ret <= 0)) {
+ goto fail;
+ }
+
+ break;
+#else
+ (void) pctx;
+ (void) olen;
+ (void) &string_info;
+ (void) &info;
+#endif
+
+ case NJS_ALGORITHM_ECDH:
+ default:
+ njs_internal_error(vm, "not implemented deriveKey "
+ "algorithm: \"%V\"", njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ if (derive_key) {
+ if (dalg->type == NJS_ALGORITHM_HMAC) {
+ ret = njs_algorithm_hash(vm, dobject, &dkey->hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, k, length);
+ if (njs_slow_path(pkey == NULL)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_new_mac_key() failed");
+ goto fail;
+ }
+
+ cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+ if (cln == NULL) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ cln->handler = njs_webcrypto_cleanup_pkey;
+ cln->data = key;
+
+ dkey->pkey = pkey;
+
+ } else {
+ dkey->raw.start = k;
+ dkey->raw.length = length;
+ }
+
+ ret = njs_vm_external_create(vm, &value,
+ njs_webcrypto_crypto_key_proto_id,
+ dkey, 0);
+ } else {
+ ret = njs_vm_value_array_buffer_set(vm, &value, k, length);
+ }
+
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_ext_digest(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t unused)
+{
+ unsigned olen;
+ u_char *dst;
+ njs_str_t data;
+ njs_int_t ret;
+ njs_value_t value;
+ const EVP_MD *md;
+ njs_webcrypto_hash_t hash;
+
+ ret = njs_algorithm_hash(vm, njs_arg(args, nargs, 1), &hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 2));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ md = njs_algorithm_hash_digest(hash);
+ olen = EVP_MD_size(md);
+
+ dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ ret = EVP_Digest(data.start, data.length, dst, &olen, md, NULL);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_Digest() failed");
+ goto fail;
+ }
+
+ ret = njs_vm_value_array_buffer_set(vm, &value, dst, olen);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+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_internal_error(vm, "\"exportKey\" not implemented");
+ return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t unused)
+{
+ njs_internal_error(vm, "\"generateKey\" not implemented");
+ return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t unused)
+{
+ int nid;
+ BIO *bio;
+ RSA *rsa;
+ EC_KEY *ec;
+ unsigned usage;
+ EVP_PKEY *pkey;
+ njs_int_t ret;
+ njs_str_t key_data, format;
+ njs_value_t value, *options;
+ const u_char *start;
+ const EC_GROUP *group;
+ njs_mp_cleanup_t *cln;
+ njs_webcrypto_key_t *key;
+ PKCS8_PRIV_KEY_INFO *pkcs8;
+ njs_webcrypto_algorithm_t *alg;
+ njs_webcrypto_key_format_t fmt;
+
+ static const int curves[] = {
+ NID_X9_62_prime256v1,
+ NID_secp384r1,
+ NID_secp521r1,
+ };
+
+ pkey = NULL;
+
+ fmt = njs_key_format(vm, njs_arg(args, nargs, 1), &format);
+ if (njs_slow_path(fmt == NJS_KEY_FORMAT_UNKNOWN)) {
+ njs_type_error(vm, "unknown key format: \"%V\"", &format);
+ goto fail;
+ }
+
+ options = njs_arg(args, nargs, 3);
+ alg = njs_key_algorithm(vm, options);
+ if (njs_slow_path(alg == NULL)) {
+ goto fail;
+ }
+
+ if (njs_slow_path(!(fmt & alg->fmt))) {
+ njs_type_error(vm, "unsupported key fmt for \"%V\" key",
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ if (njs_slow_path(usage & ~alg->usage)) {
+ njs_type_error(vm, "unsupported key usage for \"%V\" key",
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &key_data, njs_arg(args, nargs, 2));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ start = key_data.start;
+
+ switch (fmt) {
+ case NJS_KEY_FORMAT_PKCS8:
+ bio = BIO_new_mem_buf(start, key_data.length);
+ if (njs_slow_path(bio == NULL)) {
+ njs_webcrypto_error(vm, "BIO_new_mem_buf() failed");
+ goto fail;
+ }
+
+ pkcs8 = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL);
+ if (njs_slow_path(pkcs8 == NULL)) {
+ BIO_free(bio);
+ njs_webcrypto_error(vm, "d2i_PKCS8_PRIV_KEY_INFO_bio() failed");
+ goto fail;
+ }
+
+ pkey = EVP_PKCS82PKEY(pkcs8);
+ if (njs_slow_path(pkey == NULL)) {
+ PKCS8_PRIV_KEY_INFO_free(pkcs8);
+ BIO_free(bio);
+ njs_webcrypto_error(vm, "EVP_PKCS82PKEY() failed");
+ goto fail;
+ }
+
+ PKCS8_PRIV_KEY_INFO_free(pkcs8);
+ BIO_free(bio);
+
+ break;
+
+ case NJS_KEY_FORMAT_SPKI:
+ pkey = d2i_PUBKEY(NULL, &start, key_data.length);
+ if (njs_slow_path(pkey == NULL)) {
+ njs_webcrypto_error(vm, "d2i_PUBKEY() failed");
+ goto fail;
+ }
+
+ break;
+
+ case NJS_KEY_FORMAT_RAW:
+ break;
+
+ default:
+ njs_internal_error(vm, "not implemented key format: \"%V\"", &format);
+ goto fail;
+ }
+
+ key = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(njs_webcrypto_key_t));
+ if (njs_slow_path(key == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ key->alg = alg;
+ key->usage = usage;
+
+ switch (alg->type) {
+ case NJS_ALGORITHM_RSA_OAEP:
+ case NJS_ALGORITHM_RSA_PSS:
+ case NJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+ rsa = EVP_PKEY_get1_RSA(pkey);
+ if (njs_slow_path(rsa == NULL)) {
+ njs_webcrypto_error(vm, "RSA key is not found");
+ goto fail;
+ }
+
+ RSA_free(rsa);
+
+ ret = njs_algorithm_hash(vm, options, &key->hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ key->pkey = pkey;
+
+ break;
+
+ case NJS_ALGORITHM_ECDSA:
+ case NJS_ALGORITHM_ECDH:
+ ec = EVP_PKEY_get1_EC_KEY(pkey);
+ if (njs_slow_path(ec == NULL)) {
+ njs_webcrypto_error(vm, "EC key is not found");
+ goto fail;
+ }
+
+ group = EC_KEY_get0_group(ec);
+ nid = EC_GROUP_get_curve_name(group);
+ EC_KEY_free(ec);
+
+ ret = njs_algorithm_curve(vm, options, &key->curve);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ if (njs_slow_path(curves[key->curve] != nid)) {
+ njs_webcrypto_error(vm, "name curve mismatch");
+ goto fail;
+ }
+
+ key->pkey = pkey;
+
+ break;
+
+ case NJS_ALGORITHM_HMAC:
+ pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, key_data.start,
+ key_data.length);
+ if (njs_slow_path(pkey == NULL)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_new_mac_key() failed");
+ goto fail;
+ }
+
+ ret = njs_algorithm_hash(vm, options, &key->hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ key->pkey = pkey;
+
+ break;
+
+ case NJS_ALGORITHM_AES_GCM:
+ case NJS_ALGORITHM_AES_CTR:
+ case NJS_ALGORITHM_AES_CBC:
+ case NJS_ALGORITHM_PBKDF2:
+ case NJS_ALGORITHM_HKDF:
+ key->raw = key_data;
+ default:
+ break;
+ }
+
+ if (pkey != NULL) {
+ cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+ if (cln == NULL) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ cln->handler = njs_webcrypto_cleanup_pkey;
+ cln->data = key;
+ pkey = NULL;
+ }
+
+ ret = njs_vm_external_create(vm, &value, njs_webcrypto_crypto_key_proto_id,
+ key, 0);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+ if (pkey != NULL) {
+ EVP_PKEY_free(pkey);
+ }
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+static njs_int_t
+njs_set_rsa_padding(njs_vm_t *vm, njs_value_t *options, EVP_PKEY *pkey,
+ EVP_PKEY_CTX *ctx, njs_webcrypto_alg_t type)
+{
+ int padding;
+ int64_t salt_length;
+ njs_int_t ret;
+ njs_value_t value;
+
+ static const njs_value_t string_saltl = njs_string("saltLength");
+
+ if (type == NJS_ALGORITHM_ECDSA) {
+ return NJS_OK;
+ }
+
+ padding = (type == NJS_ALGORITHM_RSA_PSS) ? RSA_PKCS1_PSS_PADDING
+ : RSA_PKCS1_PADDING;
+ ret = EVP_PKEY_CTX_set_rsa_padding(ctx, padding);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_rsa_padding() failed");
+ return NJS_ERROR;
+ }
+
+ if (padding == RSA_PKCS1_PSS_PADDING) {
+ ret = njs_value_property(vm, options, njs_value_arg(&string_saltl),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "RSA-PSS algorithm.saltLength "
+ "is not provided");
+ }
+
+ return NJS_ERROR;
+ }
+
+ ret = njs_value_to_integer(vm, &value, &salt_length);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ ret = EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm,
+ "EVP_PKEY_CTX_set_rsa_pss_saltlen() failed");
+ return NJS_ERROR;
+ }
+ }
+
+ return NJS_OK;
+}
+
+
+static njs_int_t
+njs_ext_sign(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t verify)
+{
+ u_char *dst;
+ size_t olen, outlen;
+ unsigned mask, m_len;
+ njs_int_t ret;
+ njs_str_t data, sig;
+ EVP_MD_CTX *mctx;
+ njs_value_t value, *options;
+ EVP_PKEY_CTX *pctx;
+ const EVP_MD *md;
+ njs_webcrypto_key_t *key;
+ njs_webcrypto_hash_t hash;
+ njs_webcrypto_algorithm_t *alg;
+ unsigned char m[EVP_MAX_MD_SIZE];
+
+ mctx = NULL;
+ pctx = NULL;
+
+ options = njs_arg(args, nargs, 1);
+ alg = njs_key_algorithm(vm, options);
+ if (njs_slow_path(alg == NULL)) {
+ 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_type_error(vm, "\"key\" is not a CryptoKey object");
+ goto fail;
+ }
+
+ mask = verify ? NJS_KEY_USAGE_VERIFY : NJS_KEY_USAGE_SIGN;
+ if (njs_slow_path(!(key->usage & mask))) {
+ njs_type_error(vm, "provide key does not support \"sign\" operation");
+ goto fail;
+ }
+
+ if (njs_slow_path(key->alg != alg)) {
+ njs_type_error(vm, "cannot %s using \"%V\" with \"%V\" key",
+ verify ? "verify" : "sign",
+ njs_algorithm_string(key->alg),
+ njs_algorithm_string(alg));
+ goto fail;
+ }
+
+ if (verify) {
+ ret = njs_vm_value_to_bytes(vm, &sig, njs_arg(args, nargs, 3));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 4));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ } else {
+ ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 3));
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+ }
+
+ mctx = njs_evp_md_ctx_new();
+ if (njs_slow_path(mctx == NULL)) {
+ njs_webcrypto_error(vm, "njs_evp_md_ctx_new() failed");
+ goto fail;
+ }
+
+ if (alg->type == NJS_ALGORITHM_ECDSA) {
+ ret = njs_algorithm_hash(vm, options, &hash);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ goto fail;
+ }
+
+ } else {
+ hash = key->hash;
+ }
+
+ md = njs_algorithm_hash_digest(hash);
+
+ ret = EVP_DigestSignInit(mctx, NULL, md, NULL, key->pkey);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_DigestSignInit() failed");
+ goto fail;
+ }
+
+ ret = EVP_DigestSignUpdate(mctx, data.start, data.length);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_DigestSignUpdate() failed");
+ goto fail;
+ }
+
+ outlen = 0;
+
+ switch (alg->type) {
+ case NJS_ALGORITHM_HMAC:
+ olen = EVP_MD_size(md);
+
+ if (!verify) {
+ dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ } else {
+ dst = (u_char *) &m[0];
+ }
+
+ ret = EVP_DigestSignFinal(mctx, dst, &outlen);
+ if (njs_slow_path(ret <= 0 || olen != outlen)) {
+ njs_webcrypto_error(vm, "EVP_DigestSignFinal() failed");
+ goto fail;
+ }
+
+ if (verify) {
+ ret = (sig.length == outlen && memcmp(sig.start, dst, outlen) == 0);
+ }
+
+ break;
+
+ case NJS_ALGORITHM_RSASSA_PKCS1_v1_5:
+ case NJS_ALGORITHM_RSA_PSS:
+ case NJS_ALGORITHM_ECDSA:
+ default:
+ ret = EVP_DigestFinal_ex(mctx, m, &m_len);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_DigestFinal_ex() failed");
+ goto fail;
+ }
+
+ olen = EVP_PKEY_size(key->pkey);
+ dst = njs_mp_zalloc(njs_vm_memory_pool(vm), olen);
+ if (njs_slow_path(dst == NULL)) {
+ njs_memory_error(vm);
+ goto fail;
+ }
+
+ pctx = EVP_PKEY_CTX_new(key->pkey, NULL);
+ if (njs_slow_path(pctx == NULL)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed");
+ goto fail;
+ }
+
+ if (!verify) {
+ ret = EVP_PKEY_sign_init(pctx);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_sign_init() failed");
+ goto fail;
+ }
+
+ } else {
+ ret = EVP_PKEY_verify_init(pctx);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_verify_init() failed");
+ goto fail;
+ }
+ }
+
+ ret = njs_set_rsa_padding(vm, options, key->pkey, pctx, alg->type);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ ret = EVP_PKEY_CTX_set_signature_md(pctx, md);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_signature_md() failed");
+ goto fail;
+ }
+
+ if (!verify) {
+ outlen = olen;
+ ret = EVP_PKEY_sign(pctx, dst, &outlen, m, m_len);
+ if (njs_slow_path(ret <= 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_sign() failed");
+ goto fail;
+ }
+
+ } else {
+ ret = EVP_PKEY_verify(pctx, sig.start, sig.length, m, m_len);
+ if (njs_slow_path(ret < 0)) {
+ njs_webcrypto_error(vm, "EVP_PKEY_verify() failed");
+ goto fail;
+ }
+ }
+
+ EVP_PKEY_CTX_free(pctx);
+
+ break;
+ }
+
+ if (!verify) {
+ ret = njs_vm_value_array_buffer_set(vm, &value, dst, outlen);
+ if (njs_slow_path(ret != NJS_OK)) {
+ goto fail;
+ }
+
+ } else {
+ njs_set_boolean(&value, ret != 0);
+ }
+
+ njs_evp_md_ctx_free(mctx);
+
+ return njs_webcrypto_result(vm, &value, NJS_OK);
+
+fail:
+
+ if (mctx != NULL) {
+ njs_evp_md_ctx_free(mctx);
+ }
+
+ if (pctx != NULL) {
+ EVP_PKEY_CTX_free(pctx);
+ }
+
+ return njs_webcrypto_result(vm, njs_vm_retval(vm), NJS_ERROR);
+}
+
+
+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_internal_error(vm, "\"unwrapKey\" 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_internal_error(vm, "\"wrapKey\" not implemented");
+ return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_ext_get_random_values(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_index_t unused)
+{
+ njs_int_t ret;
+ njs_str_t fill;
+
+ ret = njs_vm_value_to_bytes(vm, &fill, njs_arg(args, nargs, 1));
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ if (njs_slow_path(fill.length > 65536)) {
+ njs_type_error(vm, "requested length exceeds 65536 bytes");
+ return NJS_ERROR;
+ }
+
+ if (RAND_bytes(fill.start, fill.length) != 1) {
+ njs_webcrypto_error(vm, "RAND_bytes() failed");
+ return NJS_ERROR;
+ }
+
+ return NJS_OK;
+}
+
+
+static void
+njs_webcrypto_cleanup_pkey(void *data)
+{
+ njs_webcrypto_key_t *key = data;
+
+ if (key->pkey != NULL) {
+ EVP_PKEY_free(key->pkey);
+ }
+}
+
+
+static njs_webcrypto_key_format_t
+njs_key_format(njs_vm_t *vm, njs_value_t *value, njs_str_t *format)
+{
+ njs_int_t ret;
+ njs_uint_t fmt;
+
+ static const struct {
+ njs_str_t name;
+ njs_uint_t value;
+ } formats[] = {
+ { njs_str("raw"), NJS_KEY_FORMAT_RAW },
+ { njs_str("pkcs8"), NJS_KEY_FORMAT_PKCS8 },
+ { njs_str("spki"), NJS_KEY_FORMAT_SPKI },
+ { njs_str("jwk"), NJS_KEY_FORMAT_JWK },
+ };
+
+ ret = njs_value_to_string(vm, value, value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ njs_string_get(value, format);
+
+ fmt = 0;
+
+ while (fmt < sizeof(formats) / sizeof(formats[0])) {
+ if (njs_strstr_eq(format, &formats[fmt].name)) {
+ return formats[fmt].value;
+ }
+
+ fmt++;
+ }
+
+ return NJS_KEY_FORMAT_UNKNOWN;
+}
+
+
+static njs_int_t
+njs_key_usage_array_handler(njs_vm_t *vm, njs_iterator_args_t *args,
+ njs_value_t *value, int64_t index)
+{
+ unsigned *mask;
+ njs_str_t u;
+ njs_int_t ret;
+ njs_value_t usage;
+ njs_webcrypto_entry_t *e;
+
+ njs_value_assign(&usage, value);
+
+ ret = njs_value_to_string(vm, &usage, &usage);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ njs_string_get(&usage, &u);
+
+ for (e = &njs_webcrypto_usage[0]; e->name.length != 0; e++) {
+ if (njs_strstr_eq(&u, &e->name)) {
+ mask = args->data;
+ *mask |= e->value;
+ return NJS_OK;
+ }
+ }
+
+ njs_type_error(vm, "unknown key usage: \"%V\"", &u);
+
+ return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_key_usage(njs_vm_t *vm, njs_value_t *value, unsigned *mask)
+{
+ int64_t length;
+ njs_int_t ret;
+ njs_iterator_args_t args;
+
+ ret = njs_object_length(vm, value, &length);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ *mask = 0;
+
+ args.value = value;
+ args.from = 0;
+ args.to = length;
+ args.data = mask;
+
+ return njs_object_iterate(vm, &args, njs_key_usage_array_handler);
+}
+
+
+static njs_webcrypto_algorithm_t *
+njs_key_algorithm(njs_vm_t *vm, njs_value_t *options)
+{
+ njs_int_t ret;
+ njs_str_t a;
+ njs_value_t name;
+ njs_webcrypto_entry_t *e;
+ njs_webcrypto_algorithm_t *alg;
+
+ static const njs_value_t string_name = njs_string("name");
+
+ if (njs_is_object(options)) {
+ ret = njs_value_property(vm, options, njs_value_arg(&string_name),
+ &name);
+ if (njs_slow_path(ret != NJS_OK)) {
+ if (ret == NJS_DECLINED) {
+ njs_type_error(vm, "algorithm name is not provided");
+ }
+
+ return NULL;
+ }
+
+ } else {
+ njs_value_assign(&name, options);
+ }
+
+ ret = njs_value_to_string(vm, &name, &name);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NULL;
+ }
+
+ njs_string_get(&name, &a);
+
+ for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) {
+ if (njs_strstr_case_eq(&a, &e->name)) {
+ alg = (njs_webcrypto_algorithm_t *) e->value;
+ if (alg->usage & NJS_KEY_USAGE_UNSUPPORTED) {
+ njs_type_error(vm, "unsupported algorithm: \"%V\"", &a);
+ return NULL;
+ }
+
+ return alg;
+ }
+ }
+
+ njs_type_error(vm, "unknown algorithm name: \"%V\"", &a);
+
+ return NULL;
+}
+
+
+static njs_str_t *
+njs_algorithm_string(njs_webcrypto_algorithm_t *algorithm)
+{
+ njs_webcrypto_entry_t *e;
+ njs_webcrypto_algorithm_t *alg;
+
+ for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) {
+ alg = (njs_webcrypto_algorithm_t *) e->value;
+ if (alg->type == algorithm->type) {
+ break;
+ }
+ }
+
+ return &e->name;
+}
+
+
+static njs_int_t
+njs_algorithm_hash(njs_vm_t *vm, njs_value_t *options,
+ njs_webcrypto_hash_t *hash)
+{
+ njs_int_t ret;
+ njs_str_t name;
+ njs_value_t value;
+ njs_webcrypto_entry_t *e;
+
+ static const njs_value_t string_hash = njs_string("hash");
+
+ if (njs_is_object(options)) {
+ ret = njs_value_property(vm, options, njs_value_arg(&string_hash),
+ &value);
+ if (njs_slow_path(ret == NJS_ERROR)) {
+ return NJS_ERROR;
+ }
+
+ } else {
+ njs_value_assign(&value, options);
+ }
+
+ ret = njs_value_to_string(vm, &value, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ njs_string_get(&value, &name);
+
+ for (e = &njs_webcrypto_hash[0]; e->name.length != 0; e++) {
+ if (njs_strstr_eq(&name, &e->name)) {
+ *hash = e->value;
+ return NJS_OK;
+ }
+ }
+
+ njs_type_error(vm, "unknown hash name: \"%V\"", &name);
+
+ return NJS_ERROR;
+}
+
+
+static const EVP_MD *
+njs_algorithm_hash_digest(njs_webcrypto_hash_t hash)
+{
+ switch (hash) {
+ case NJS_HASH_SHA256:
+ return EVP_sha256();
+
+ case NJS_HASH_SHA384:
+ return EVP_sha384();
+
+ case NJS_HASH_SHA512:
+ return EVP_sha512();
+
+ case NJS_HASH_SHA1:
+ default:
+ break;
+ }
+
+ return EVP_sha1();
+}
+
+
+static njs_int_t
+njs_algorithm_curve(njs_vm_t *vm, njs_value_t *options,
+ njs_webcrypto_curve_t *curve)
+{
+ njs_int_t ret;
+ njs_str_t name;
+ njs_value_t value;
+ njs_webcrypto_entry_t *e;
+
+ static const njs_value_t string_curve = njs_string("namedCurve");
+
+ ret = njs_value_property(vm, options, njs_value_arg(&string_curve),
+ &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return ret;
+ }
+
+ ret = njs_value_to_string(vm, &value, &value);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ njs_string_get(&value, &name);
+
+ for (e = &njs_webcrypto_curve[0]; e->name.length != 0; e++) {
+ if (njs_strstr_eq(&name, &e->name)) {
+ *curve = e->value;
+ return NJS_OK;
+ }
+ }
+
+ njs_type_error(vm, "unknown namedCurve: \"%V\"", &name);
+
+ return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_promise_trampoline(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused)
+{
+ njs_function_t *callback;
+
+ callback = njs_value_function(njs_argument(args, 1));
+
+ if (callback != NULL) {
+ return njs_vm_call(vm, callback, njs_argument(args, 2), 1);
+ }
+
+ return NJS_OK;
+}
+
+
+static njs_int_t
+njs_webcrypto_result(njs_vm_t *vm, njs_value_t *result, njs_int_t rc)
+{
+ njs_int_t ret;
+ njs_value_t retval, arguments[2];
+ njs_function_t *callback;
+ njs_vm_event_t vm_event;
+
+ ret = njs_vm_promise_create(vm, &retval, njs_value_arg(&arguments));
+ if (ret != NJS_OK) {
+ goto error;
+ }
+
+ callback = njs_vm_function_alloc(vm, njs_promise_trampoline);
+ if (callback == NULL) {
+ goto error;
+ }
+
+ vm_event = njs_vm_add_event(vm, callback, 1, NULL, NULL);
+ if (vm_event == NULL) {
+ goto error;
+ }
+
+ njs_value_assign(&arguments[0], &arguments[(rc != NJS_OK)]);
+ njs_value_assign(&arguments[1], result);
+
+ ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2);
+ if (ret == NJS_ERROR) {
+ goto error;
+ }
+
+ njs_vm_retval_set(vm, njs_value_arg(&retval));
+
+ return NJS_OK;
+
+error:
+
+ njs_vm_error(vm, "internal error");
+
+ return NJS_ERROR;
+}
+
+
+static u_char *
+njs_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
+njs_webcrypto_error(njs_vm_t *vm, 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 = njs_cpystrn(p, (u_char *) " (SSL:", last - p);
+
+ for ( ;; ) {
+
+ n = ERR_peek_error_line_data(NULL, NULL, &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 = njs_cpystrn(p, (u_char *) data, last - p);
+ }
+
+ next:
+
+ (void) ERR_get_error();
+ }
+
+ if (p < last) {
+ *p++ = ')';
+ }
+ }
+
+ njs_vm_value_error_set(vm, njs_vm_retval(vm), "%*s", p - errstr, errstr);
+}
+
+
+njs_int_t
+njs_external_webcrypto_init(njs_vm_t *vm)
+{
+ njs_int_t ret, proto_id;
+ njs_str_t name;
+ njs_opaque_value_t value;
+
+ OpenSSL_add_all_algorithms();
+
+ njs_webcrypto_crypto_key_proto_id =
+ njs_vm_external_prototype(vm, njs_ext_webcrypto_crypto_key,
+ njs_nitems(njs_ext_webcrypto_crypto_key));
+ if (njs_slow_path(njs_webcrypto_crypto_key_proto_id < 0)) {
+ return NJS_ERROR;
+ }
+
+ proto_id = njs_vm_external_prototype(vm, njs_ext_webcrypto,
+ njs_nitems(njs_ext_webcrypto));
+ if (njs_slow_path(proto_id < 0)) {
+ return NJS_ERROR;
+ }
+
+ ret = njs_vm_external_create(vm, njs_value_arg(&value), proto_id, NULL, 1);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ name.length = njs_length("crypto");
+ name.start = (u_char *) "crypto";
+
+ ret = njs_vm_bind(vm, &name, njs_value_arg(&value), 1);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+
+ return NJS_OK;
+}
diff --git a/external/njs_webcrypto.h b/external/njs_webcrypto.h
new file mode 100644
index 0000000..3331b57
--- /dev/null
+++ b/external/njs_webcrypto.h
@@ -0,0 +1,15 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#ifndef _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_
+#define _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_
+
+
+njs_int_t njs_external_webcrypto_init(njs_vm_t *vm);
+
+
+#endif /* _NJS_EXTERNAL_WEBCRYPTO_H_INCLUDED_ */
diff --git a/nginx/config b/nginx/config
index 29f6dca..7ebdcfe 100644
--- a/nginx/config
+++ b/nginx/config
@@ -1,8 +1,11 @@
ngx_addon_name="ngx_js_module"
-NJS_DEPS="$ngx_addon_dir/ngx_js.h"
+NJS_DEPS="$ngx_addon_dir/ngx_js.h \
+ $ngx_addon_dir/ngx_js_fetch.h \
+ $ngx_addon_dir/../external/njs_webcrypto.h"
NJS_SRCS="$ngx_addon_dir/ngx_js.c \
- $ngx_addon_dir/ngx_js_fetch.c"
+ $ngx_addon_dir/ngx_js_fetch.c \
+ $ngx_addon_dir/../external/njs_webcrypto.c"
if [ $HTTP != NO ]; then
ngx_module_type=HTTP_AUX_FILTER
@@ -10,7 +13,7 @@
ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS"
- ngx_module_libs="PCRE $ngx_addon_dir/../build/libnjs.a -lm"
+ ngx_module_libs="PCRE OPENSSL $ngx_addon_dir/../build/libnjs.a -lm"
. auto/module
@@ -25,7 +28,7 @@
ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS"
- ngx_module_libs="PCRE $ngx_addon_dir/../build/libnjs.a -lm"
+ ngx_module_libs="PCRE OPENSSL $ngx_addon_dir/../build/libnjs.a -lm"
. auto/module
fi
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c
index ae7955e..012adb4 100644
--- a/nginx/ngx_js.c
+++ b/nginx/ngx_js.c
@@ -10,6 +10,7 @@
#include <ngx_core.h>
#include "ngx_js.h"
#include "ngx_js_fetch.h"
+#include "../external/njs_webcrypto.h"
static njs_external_t ngx_js_ext_core[] = {
@@ -176,6 +177,12 @@
return NGX_ERROR;
}
+ ret = njs_external_webcrypto_init(vm);
+ if (ret != NJS_OK) {
+ ngx_log_error(NGX_LOG_EMERG, log, 0, "failed to add webcrypto object");
+ return NGX_ERROR;
+ }
+
proto_id = njs_vm_external_prototype(vm, ngx_js_ext_core,
njs_nitems(ngx_js_ext_core));
if (proto_id < 0) {
diff --git a/src/njs_shell.c b/src/njs_shell.c
index bfa9a37..5abb171 100644
--- a/src/njs_shell.c
+++ b/src/njs_shell.c
@@ -23,6 +23,11 @@
#endif
+#if (NJS_HAVE_OPENSSL)
+#include "../external/njs_webcrypto.h"
+#include "../external/njs_webcrypto.c"
+#endif
+
typedef struct {
uint8_t disassemble;
@@ -718,6 +723,13 @@
return NJS_ERROR;
}
+#if (NJS_HAVE_OPENSSL)
+ ret = njs_external_webcrypto_init(vm);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+#endif
+
return NJS_OK;
}
diff --git a/src/njs_str.c b/src/njs_str.c
new file mode 100644
index 0000000..fdad373
--- /dev/null
+++ b/src/njs_str.c
@@ -0,0 +1,37 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <njs_main.h>
+
+
+njs_int_t
+njs_strncasecmp(u_char *s1, u_char *s2, size_t n)
+{
+ njs_uint_t c1, c2;
+
+ while (n) {
+ c1 = (njs_uint_t) *s1++;
+ c2 = (njs_uint_t) *s2++;
+
+ c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;
+ c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;
+
+ if (c1 == c2) {
+
+ if (c1) {
+ n--;
+ continue;
+ }
+
+ return 0;
+ }
+
+ return c1 - c2;
+ }
+
+ return 0;
+}
diff --git a/src/njs_str.h b/src/njs_str.h
index 5bc013d..ae4dd08 100644
--- a/src/njs_str.h
+++ b/src/njs_str.h
@@ -134,4 +134,13 @@
&& (memcmp((s1)->start, (s2)->start, (s1)->length) == 0))
+#define \
+njs_strstr_case_eq(s1, s2) \
+ (((s1)->length == (s2)->length) \
+ && (njs_strncasecmp((s1)->start, (s2)->start, (s1)->length) == 0))
+
+
+NJS_EXPORT njs_int_t njs_strncasecmp(u_char *s1, u_char *s2, size_t n);
+
+
#endif /* _NJS_STR_H_INCLUDED_ */
diff --git a/src/test/njs_externals_test.c b/src/test/njs_externals_test.c
index c52e65b..3285b16 100644
--- a/src/test/njs_externals_test.c
+++ b/src/test/njs_externals_test.c
@@ -8,6 +8,11 @@
#include "njs_externals_test.h"
+#if (NJS_HAVE_OPENSSL)
+#include "../external/njs_webcrypto.h"
+#include "../external/njs_webcrypto.c"
+#endif
+
typedef struct {
njs_lvlhsh_t hash;
@@ -833,6 +838,15 @@
njs_int_t
njs_externals_shared_init(njs_vm_t *vm)
{
+#if (NJS_HAVE_OPENSSL)
+ njs_int_t ret;
+
+ ret = njs_external_webcrypto_init(vm);
+ if (njs_slow_path(ret != NJS_OK)) {
+ return NJS_ERROR;
+ }
+#endif
+
return njs_externals_init_internal(vm, njs_test_requests, 1, 1);
}
diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c
index ae97e76..4309c50 100644
--- a/src/test/njs_unit_test.c
+++ b/src/test/njs_unit_test.c
@@ -20621,6 +20621,26 @@
};
+static njs_unit_test_t njs_webcrypto_test[] =
+{
+ /* Statistic test
+ * bits1 is a random variable with Binomial distribution
+ * Expected value is N / 2
+ * Standard deviation is sqrt(N / 4)
+ */
+ { njs_str("function count1(v) {return v.toString(2).match(/1/g).length;}"
+ "let buf = new Uint32Array(32);"
+ "crypto.getRandomValues(buf);"
+ "let bits1 = buf.reduce((a, v)=> a + count1(v), 0);"
+ "let nbits = buf.length * 32;"
+ "let mean = nbits / 2;"
+ "let stddev = Math.sqrt(nbits / 4);"
+ "let condition = bits1 > (mean - 10 * stddev) && bits1 < (mean + 10 * stddev);"
+ "condition ? true : [buf, nbits, bits1, mean, stddev]"),
+ njs_str("true") },
+};
+
+
static njs_unit_test_t njs_module_test[] =
{
{ njs_str("function f(){return 2}; var f; f()"),
@@ -20854,13 +20874,21 @@
{ njs_str("$r2.uri == 'αβγ' && $r2.uri === 'αβγ'"),
njs_str("true") },
- { njs_str("Object.keys(this).sort()"),
#if (NJS_TEST262)
- njs_str("$262,$r,$r2,$r3,$shared,global,njs,process") },
+#define N262 "$262,"
#else
- njs_str("$r,$r2,$r3,$shared,global,njs,process") },
+#define N262 ""
#endif
+#if (NJS_HAVE_OPENSSL)
+#define NCRYPTO "crypto,"
+#else
+#define NCRYPTO ""
+#endif
+
+ { njs_str("Object.keys(this).sort()"),
+ njs_str(N262 "$r,$r2,$r3,$shared," NCRYPTO "global,njs,process") },
+
{ njs_str("Object.getOwnPropertySymbols($r2)[0] == Symbol.toStringTag"),
njs_str("true") },
@@ -23246,6 +23274,17 @@
njs_nitems(njs_disabled_denormals_test),
njs_disabled_denormals_tests },
+ {
+#if (NJS_HAVE_OPENSSL)
+ njs_str("webcrypto"),
+#else
+ njs_str(""),
+#endif
+ { .externals = 1, .repeat = 1, .unsafe = 1 },
+ njs_webcrypto_test,
+ njs_nitems(njs_webcrypto_test),
+ njs_unit_test },
+
{ njs_str("module"),
{ .repeat = 1, .module = 1, .unsafe = 1 },
njs_module_test,
diff --git a/test/njs_expect_test.exp b/test/njs_expect_test.exp
index cc3bebe..9b2287a 100644
--- a/test/njs_expect_test.exp
+++ b/test/njs_expect_test.exp
@@ -1113,6 +1113,38 @@
njs_run {"./test/js/promise_race_throw.js"} \
"rejected:one"
+# Webcrypto
+
+njs_run {"./test/webcrypto/rsa_decoding.js" "--match-exception-text"} \
+"RSA-OAEP decoding SUCCESS"
+
+njs_run {"./test/webcrypto/rsa.js" "--match-exception-text"} \
+"RSA-OAEP encoding/decoding SUCCESS"
+
+njs_run {"./test/webcrypto/aes_decoding.js" "--match-exception-text"} \
+"AES decoding SUCCESS"
+
+njs_run {"./test/webcrypto/aes.js" "--match-exception-text"} \
+"AES encoding/decoding SUCCESS"
+
+njs_run {"./test/webcrypto/derive.js" "--match-exception-text"} \
+"derive SUCCESS"
+
+njs_run {"./test/webcrypto/digest.js" "--match-exception-text"} \
+"SHA digest SUCCESS"
+
+njs_run {"./test/webcrypto/sign.js" "--match-exception-text"} \
+"HMAC sign SUCCESS
+RSASSA-PKCS1-v1_5 sign SUCCESS
+RSA-PSS sign SUCCESS
+ECDSA sign SUCCESS"
+
+njs_run {"./test/webcrypto/verify.js" "--match-exception-text"} \
+"HMAC verify SUCCESS
+RSASSA-PKCS1-v1_5 verify SUCCESS
+RSA-PSS verify SUCCESS
+ECDSA verify SUCCESS"
+
# Async/Await
njs_run {"./test/js/async_await_inline.js"} \
diff --git a/test/ts/test.ts b/test/ts/test.ts
index 4ec9db0..a30c588 100644
--- a/test/ts/test.ts
+++ b/test/ts/test.ts
@@ -1,6 +1,6 @@
import fs from 'fs';
import qs from 'querystring';
-import crypto from 'crypto';
+import cr from 'crypto';
function http_module(r: NginxHTTPRequest) {
var bs: NjsByteString;
@@ -122,11 +122,30 @@
var b:Buffer;
var s:string;
- h = crypto.createHash("sha1");
+ h = cr.createHash("sha1");
h = h.update(str).update(Buffer.from([0]));
b = h.digest();
- s = crypto.createHash("sha256").digest("hex");
+ s = cr.createHash("sha256").digest("hex");
+}
+
+async function crypto_object(keyData: ArrayBuffer, data: ArrayBuffer) {
+ let iv = crypto.getRandomValues(new Uint8Array(16));
+
+ let ekey = await crypto.subtle.importKey("pkcs8", keyData,
+ {name: 'RSA-OAEP', hash: "SHA-256"},
+ false, ['decrypt']);
+
+ let skey = await crypto.subtle.importKey("raw", keyData, 'AES-CBC',
+ false, ['encrypt']);
+
+ data = await crypto.subtle.decrypt({name: 'RSA-OAEP'}, ekey, data);
+ data = await crypto.subtle.encrypt({name: 'AES-CBC', iv:iv}, skey, data);
+
+ let sig = await crypto.subtle.sign({name: 'RSA-PSS', saltLength:32}, skey, data);
+
+ let r:boolean;
+ r = await crypto.subtle.verify({name: 'RSA-PSS', saltLength:32}, skey, sig, data);
}
function buffer(b: Buffer) {
diff --git a/test/webcrypto/README.rst b/test/webcrypto/README.rst
new file mode 100644
index 0000000..a06d1b4
--- /dev/null
+++ b/test/webcrypto/README.rst
@@ -0,0 +1,136 @@
+===============
+WebCrypto tests
+===============
+
+Intro
+=====
+
+Tests in this folder are expected to be compatible with node.js
+
+Tested versions
+---------------
+
+node: v16.4.0
+openssl: OpenSSL 1.1.1f 31 Mar 2020
+
+Keys generation
+===============
+
+Generating RSA PKCS8/SPKI key files
+-----------------------------------
+
+.. code-block:: shell
+
+ openssl genrsa -out rsa.pem 1024
+ openssl pkcs8 -inform PEM -in rsa.pem -nocrypt -topk8 -outform PEM -out rsa.pkcs8
+ openssl rsa -in rsa.pkcs8 -pubout > rsa.spki
+
+Generating EC PKCS8/SPKI key files
+----------------------------------
+
+.. code-block:: shell
+
+ openssl ecparam -name prime256v1 -genkey -noout -out ec.pem
+ openssl pkcs8 -inform PEM -in ec.pem -nocrypt -topk8 -outform PEM -out ec.pkcs8
+ openssl ec -in ec.pkcs8 -pubout > ec.spki
+
+Encoding
+========
+
+Encoding data using RSA-OAEP
+----------------------------
+
+.. code-block:: shell
+
+ echo -n "WAKAWAKA" > text.txt
+ openssl rsautl -inkey key.spki -pubin -in text.txt -out - -oaep -encrypt | \
+ base64 > text.base64.rsa-oaep.enc
+
+Decoding ciphertext using RSA-OAEP
+----------------------------------
+
+.. code-block:: shell
+
+ base64 -d text.base64.rsa-oaep.enc | openssl rsautl -inkey key.pkcs8 -in - -out - -oaep -decrypt
+ WAKAWAKA
+
+Encoding data using AES-GCM
+---------------------------
+
+.. code-block:: shell
+
+ echo -n "AES-GCM-SECRET-TEXT" > text.txt
+ node ./test/webcrypto/aes_gcm_enc.js '{"in":"text.txt"}' > text.base64.aes-gcm128.enc
+
+ echo -n "AES-GCM-96-TAG-LENGTH-SECRET-TEXT" > text.txt
+ node ./test/webcrypto/aes_gcm_enc.js '{"in":"text.txt","tagLength":96}' > text.base64.aes-gcm128-96.enc
+
+Encoding data using AES-CTR
+---------------------------
+
+.. code-block:: shell
+
+ echo -n "AES-CTR-SECRET-TEXT" | \
+ openssl enc -aes-128-ctr -K 00112233001122330011223300112233 -iv 44556677445566774455667744556677 | \
+ base64 > text.base64.aes-ctr128.enc
+
+Encoding data using AES-CBC
+---------------------------
+
+.. code-block:: shell
+
+ echo -n "AES-CBC-SECRET-TEXT" | \
+ openssl enc -aes-128-cbc -K 00112233001122330011223300112233 -iv 44556677445566774455667744556677 | \
+ base64 > text.base64.aes-cbc128.enc
+
+Signing
+=======
+
+Signing data using HMAC
+-----------------------
+
+.. code-block:: shell
+
+ echo -n "SigneD-TExt" > text.txt
+ openssl dgst -sha256 -mac hmac -macopt hexkey:aabbcc -binary text.txt | \
+ base64 > test/webcrypto/text.base64.sha256.hmac.sig
+
+Signing data using RSASSA-PKCS1-v1_5
+------------------------------------
+
+.. code-block:: shell
+
+ echo -n "SigneD-TExt" > text.txt
+ openssl dgst -sha256 -sigopt rsa_padding_mode:pkcs1 -sign test/webcrypto/rsa.pkcs8 text.txt | \
+ base64 > test/webcrypto/text.base64.sha256.pkcs1.sig
+ base64 -d test/webcrypto/text.base64.sha256.pkcs1.sig > text.sha256.pkcs1.sig
+ openssl dgst -sha256 -sigopt rsa_padding_mode:pkcs1 -verify test/webcrypto/rsa.spki \
+ -signature text.sha256.pkcs1.sig text.txt
+ Verified OK
+
+Signing data using RSA-PSS
+--------------------------
+
+.. code-block:: shell
+
+ echo -n "SigneD-TExt" > text.txt
+ openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sign test/webcrypto/rsa.pkcs8 text.txt | \
+ base64 > test/webcrypto/text.base64.sha256.rsa-pss.32.sig
+ base64 -d test/webcrypto/text.base64.sha256.rsa-pss.32.sig > text.sha256.rsa-pss.32.sig
+ openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 \
+ -verify test/webcrypto/rsa.spki -signature text.sha256.rsa-pss.sig text.txt
+ Verified OK
+
+Signing data using ECDSA
+------------------------
+
+.. code-block:: shell
+
+ echo -n "SigneD-TExt" > text.txt
+ openssl dgst -sha256 -binary text.txt > text.sha256
+ openssl pkeyutl -sign -in text.sha256 -inkey test/webcrypto/ec.pkcs8 | \
+ base64 > test/webcrypto/text.base64.sha256.ecdsa.sig
+ base64 -d test/webcrypto/text.base64.sha256.ecdsa.sig > text.sha256.ecdsa.sig
+ openssl pkeyutl -verify -in text.sha256 -pubin -inkey test/webcrypto/ec.spki -sigfile text.sha256.ecdsa.sig
+ Signature Verified Successfully
+
diff --git a/test/webcrypto/aes.js b/test/webcrypto/aes.js
new file mode 100644
index 0000000..1b8219f
--- /dev/null
+++ b/test/webcrypto/aes.js
@@ -0,0 +1,123 @@
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function p(args, default_opts) {
+ let params = Object.assign({}, default_opts, args);
+
+ params.key = Buffer.from(params.key, "hex");
+ params.data = Buffer.from(params.data, "hex");
+ params.iv = Buffer.from(params.iv, "hex");
+ params.counter = Buffer.from(params.counter, "hex");
+
+ switch (params.name) {
+ case "AES-GCM":
+ if (params.additionalData) {
+ params.additionalData = Buffer.from(params.additionalData, "hex");
+ }
+
+ break;
+ }
+
+ return params;
+}
+
+async function test(params) {
+ let dkey = await crypto.subtle.importKey("raw", params.key,
+ {name: params.name},
+ false, ["decrypt"]);
+
+ let ekey = await crypto.subtle.importKey("raw", params.key,
+ {name: params.name},
+ false, ["encrypt"]);
+
+ let enc = await crypto.subtle.encrypt(params, ekey, params.data);
+ let plaintext = await crypto.subtle.decrypt(params, dkey, enc);
+ plaintext = Buffer.from(plaintext);
+
+ if (params.data.compare(plaintext) != 0) {
+ throw Error(`${params.name} encoding/decoding failed length ${data.length}`);
+ }
+
+ return 'SUCCESS';
+}
+
+let aes_tsuite = {
+ name: "AES encoding/decoding",
+ opts: {
+ iv: "44556677445566774455667744556677",
+ key: "00112233001122330011223300112233",
+ counter: "44556677445566774455667744556677",
+ length: 64
+ },
+
+ tests: [
+ { name: "AES-gcm", data: "aa" },
+ { name: "aes-gcm", data: "aabbcc" },
+ { name: "AES-GCM", data: "aabbcc", additionalData: "deafbeef"},
+ { name: "AES-GCM", data: "aabbccdd".repeat(4) },
+ { name: "AES-GCM", data: "aa", iv: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
+ { name: "AES-GCM", data: "aabbcc", tagLength: 96 },
+ { name: "AES-GCM", data: "aabbcc", tagLength: 112 },
+ { name: "AES-GCM", data: "aabbcc", tagLength: 113, exception: "TypeError: AES-GCM Invalid tagLength" },
+ { name: "AES-GCM", data: "aabbccdd".repeat(4096) },
+
+ { name: "AES-CTR", data: "aa" },
+ { name: "AES-CTR", data: "aabbcc" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4) },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096) },
+ { name: "AES-CTR", data: "aa", counter: "ffffffffffffffffffffffffffffffff" },
+ { name: "AES-CTR", data: "aa", counter: "ffffffff",
+ exception: "TypeError: AES-CTR algorithm.counter must be 16 bytes long" },
+ { name: "AES-CTR", data: "aabbcc", counter: "ffffffffffffffffffffffffffffffff" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(5), counter: "ffffffffffffffffffffffffffffffff" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "fffffffffffffffffffffffffffffff0" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff", length: 7,
+ exception: "TypeError: AES-CTR repeated counter" },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), counter: "ffffffffffffffffffffffffffffffff", length: 11 },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), length: 20 },
+ { name: "AES-CTR", data: "aabbccdd".repeat(4096), length: 24 },
+ { name: "AES-CTR", data: "aabbccdd", length: 129,
+ exception: "TypeError: AES-CTR algorithm.length must be between 1 and 128" },
+
+ { name: "AES-CBC", data: "aa" },
+ { name: "AES-CBC", data: "aabbccdd".repeat(4) },
+ { name: "AES-CBC", data: "aabbccdd".repeat(4096) },
+ { name: "AES-CBC", data: "aabbccdd".repeat(5), iv: "ffffffffffffffffffffffffffffffff" },
+]};
+
+run([aes_tsuite], test, p);
diff --git a/test/webcrypto/aes_decoding.js b/test/webcrypto/aes_decoding.js
new file mode 100644
index 0000000..3b4dfe7
--- /dev/null
+++ b/test/webcrypto/aes_decoding.js
@@ -0,0 +1,116 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function base64decode(b64) {
+ const joined = b64.toString().split('\n').join('');
+ return Buffer.from(joined, 'base64');
+}
+
+function p(args, default_opts) {
+ let params = Object.assign({}, default_opts, args);
+
+ params.key = Buffer.from(params.key, "hex");
+ params.iv = Buffer.from(params.iv, "hex");
+ params.counter = Buffer.from(params.counter, "hex");
+
+ switch (params.name) {
+ case "AES-GCM":
+ if (params.additionalData) {
+ params.additionalData = Buffer.from(params.additionalData, "hex");
+ }
+
+ break;
+ }
+
+ return params;
+}
+
+async function test(params) {
+ let enc = base64decode(fs.readFileSync(`test/webcrypto/${params.file}`));
+ let key = await crypto.subtle.importKey("raw", params.key,
+ {name: params.name},
+ false, ["decrypt"]);
+
+ let plaintext = await crypto.subtle.decrypt(params, key, enc);
+ plaintext = new TextDecoder().decode(plaintext);
+
+ if (params.expected != plaintext) {
+ throw Error(`${params.name} decoding failed expected: "${params.expected}" vs "${plaintext}"`);
+ }
+
+ return 'SUCCESS';
+}
+
+let aes_tsuite = {
+ name: "AES decoding",
+ opts: {
+ key: "00112233001122330011223300112233",
+ iv: "44556677445566774455667744556677",
+ counter: "44556677445566774455667744556677",
+ length: 64
+ },
+
+ tests: [
+ { name: "AES-GCM", file: "text.base64.aes-gcm128.enc",
+ expected: "AES-GCM-SECRET-TEXT" },
+ { name: "AES-GCM", file: "text.base64.aes-gcm128-96.enc",
+ exception: "Error: EVP_DecryptFinal_ex() failed" },
+ { name: "AES-GCM", file: "text.base64.aes-gcm128-96.enc", tagLength: 96,
+ expected: "AES-GCM-96-TAG-LENGTH-SECRET-TEXT" },
+ { name: "AES-GCM", file: "text.base64.aes-gcm128-extra.enc", additionalData: "deadbeef",
+ expected: "AES-GCM-ADDITIONAL-DATA-SECRET-TEXT" },
+ { name: "AES-GCM", file: "text.base64.aes-gcm256.enc",
+ key: "0011223300112233001122330011223300112233001122330011223300112233",
+ expected: "AES-GCM-256-SECRET-TEXT" },
+ { name: "AES-GCM", file: "text.base64.aes-gcm256.enc",
+ key: "00112233001122330011223300112233001122330011223300112233001122",
+ exception: "TypeError: AES-GCM Invalid key length" },
+ { name: "AES-CTR", file: "text.base64.aes-ctr128.enc",
+ expected: "AES-CTR-SECRET-TEXT" },
+ { name: "AES-CTR", file: "text.base64.aes-ctr256.enc",
+ key: "0011223300112233001122330011223300112233001122330011223300112233",
+ expected: "AES-CTR-256-SECRET-TEXT" },
+ { name: "AES-CBC", file: "text.base64.aes-cbc128.enc",
+ expected: "AES-CBC-SECRET-TEXT" },
+ { name: "AES-CBC", file: "text.base64.aes-cbc256.enc",
+ key: "0011223300112233001122330011223300112233001122330011223300112233",
+ expected: "AES-CBC-256-SECRET-TEXT" },
+]};
+
+run([aes_tsuite], test, p);
diff --git a/test/webcrypto/aes_gcm_enc.js b/test/webcrypto/aes_gcm_enc.js
new file mode 100644
index 0000000..f286d83
--- /dev/null
+++ b/test/webcrypto/aes_gcm_enc.js
@@ -0,0 +1,51 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+function parse_options(argv) {
+ let opts = JSON.parse(argv[2] ? argv[2] : "{}");
+
+ if (!opts.key) {
+ opts.key = Buffer.from("00112233001122330011223300112233", "hex");
+
+ } else {
+ opts.key = Buffer.from(opts.key, "hex");
+ }
+
+ if (!opts.iv) {
+ opts.iv = Buffer.from("44556677445566774455667744556677", "hex");
+
+ } else {
+ opts.iv = Buffer.from(opts.iv, "hex");
+ }
+
+ if (opts.additionalData) {
+ opts.additionalData = Buffer.from(opts.additionalData, "hex");
+ }
+
+ if (!opts['in']) {
+ throw Error("opts.in is expected");
+ }
+
+ return opts;
+}
+
+(async function main() {
+ let opts = parse_options(process.argv);
+ let stdin = fs.readFileSync(`test/webcrypto/${opts['in']}`);
+ let key = await crypto.subtle.importKey("raw", opts.key,
+ {name: "AES-GCM"},
+ false, ["encrypt"]);
+
+ let params = Object.assign(opts);
+ params.name = "AES-GCM";
+
+ let enc = await crypto.subtle.encrypt(params, key, stdin);
+
+ console.log(Buffer.from(enc).toString("base64"));
+})()
+.catch(e => {
+ console.log(`exception:${e.stack}`);
+})
diff --git a/test/webcrypto/derive.js b/test/webcrypto/derive.js
new file mode 100644
index 0000000..e2f6917
--- /dev/null
+++ b/test/webcrypto/derive.js
@@ -0,0 +1,149 @@
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ if (r.status == "rejected" && t[i].optional) {
+ return r.reason.toString().startsWith("InternalError: not implemented");
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function merge(to, from) {
+ let r = Object.assign({}, to);
+ Object.keys(from).forEach(v => {
+ if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+ r[v] = merge(r[v], from[v]);
+
+ } else if (typeof from[v] == 'object') {
+ r[v] = Object.assign({}, from[v]);
+
+ } else {
+ r[v] = from[v];
+ }
+ })
+
+ return r;
+};
+
+function p(args, default_opts) {
+ let params = Object.assign({}, default_opts);
+ params = merge(params, args);
+
+ params.algorithm.salt = Buffer.from(params.algorithm.salt, "hex");
+ params.algorithm.info = Buffer.from(params.algorithm.info, "hex");
+ params.derivedAlgorithm.iv = Buffer.from(params.derivedAlgorithm.iv, "hex");
+
+ return params;
+}
+
+async function test(params) {
+ let r;
+ let encoder = new TextEncoder();
+ let keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(params.pass),
+ params.algorithm.name,
+ false, [ "deriveBits", "deriveKey" ]);
+ if (params.derive === "key") {
+ let key = await crypto.subtle.deriveKey(params.algorithm, keyMaterial,
+ params.derivedAlgorithm,
+ true, [ "encrypt", "decrypt" ]);
+
+ r = await crypto.subtle.encrypt(params.derivedAlgorithm, key,
+ encoder.encode(params.text));
+ } else {
+
+ r = await crypto.subtle.deriveBits(params.algorithm, keyMaterial, params.length);
+ }
+
+ r = Buffer.from(r).toString("hex");
+
+ if (params.expected != r) {
+ throw Error(`${params.algorithm.name} failed expected: "${params.expected}" vs "${r}"`);
+ }
+
+ return "SUCCESS";
+}
+
+let derive_tsuite = {
+ name: "derive",
+ opts: {
+ text: "secReT",
+ pass: "passW0rd",
+ derive: "key",
+ optional: false,
+ length: 256,
+ algorithm: {
+ name: "PBKDF2",
+ salt: "00112233001122330011223300112233",
+ hash: "SHA-256",
+ info: "deadbeef",
+ iterations: 100000
+ },
+ derivedAlgorithm: {
+ name: "AES-GCM",
+ length: 256,
+ iv: "55667788556677885566778855667788"
+ }
+ },
+
+ tests: [
+ { expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" },
+ { pass: "pass2", expected: "e87d1787f2807ea0e1f7e1cb265b23004c575cf2ad7e" },
+ { algorithm: { iterations: 10000 }, expected: "5add0059931ed1db1ca24c26dbe4de5719c43ed18a54" },
+ { algorithm: { hash: "SHA-512" }, expected: "544d64e5e246fdd2ba290ea932b2d80ef411c76139f4" },
+ { algorithm: { salt: "aabbccddaabbccddaabbccddaabbccdd" }, expected: "5c1304bedf840b1f6f7d1aa804fe870a8f949d762c32" },
+ { algorithm: { salt: "aabbccddaabbccddaabbccddaabb" },
+ exception: "TypeError: PBKDF2 algorithm.salt must be at least 16 bytes long" },
+ { derivedAlgorithm: { length: 128 }, expected: "9e2d7bcc1f21f30ec3c32af9129b64507d086d129f2a" },
+ { derivedAlgorithm: { length: 32 },
+ exception: "TypeError: deriveKey \"AES-GCM\" length must be 128 or 256" },
+ { derivedAlgorithm: { name: "AES-CBC" }, expected: "3ad6523692d44b6a7a90be7c2721786f" },
+
+ { derive: "bits", expected: "6458ed6e16b998d4e646422171087be8a1ee34bed463dfcb3dcd30842b1228fe" },
+ { derive: "bits", pass: "pass2", expected: "ef8f75073fcadfd504d26610c743873e297ad90340c23ddc0e5f6bdb83cbabb2" },
+ { derive: "bits", algorithm: { salt: "aabbccddaabbccddaabbccddaabbccdd" },
+ expected: "22ceb295aa25b59c6bc5b383a089bd6999006c03f273ce3614a4fa0d90bd29ae" },
+ { derive: "bits", algorithm: { hash: "SHA-1" },
+ expected: "a2fc83498f7d07b4c8180c7ebfec2af0f3a7d6cb08bf8593d41d3c5c1e1c4d67" },
+ { derive: "bits", algorithm: { hash: "SHA-1" }, length: 128,
+ expected: "a2fc83498f7d07b4c8180c7ebfec2af0" },
+ { derive: "bits", algorithm: { hash: "SHA-1" }, length: 64,
+ expected: "a2fc83498f7d07b4" },
+
+ { algorithm: { name: "HKDF" }, optional: true,
+ expected: "18ea069ee3317d2db02e02f4a228f50dc80d9a2396e6" },
+ { derive: "bits", algorithm: { name: "HKDF" }, optional: true,
+ expected: "e089c7491711306c69e077aa19fae6bfd2d4a6d240b0d37317d50472d7291a3e" },
+]};
+
+run([derive_tsuite], test, p);
diff --git a/test/webcrypto/digest.js b/test/webcrypto/digest.js
new file mode 100644
index 0000000..4eed191
--- /dev/null
+++ b/test/webcrypto/digest.js
@@ -0,0 +1,88 @@
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function p(args) {
+ let params = Object.assign({}, args);
+ params.data = Buffer.from(params.data, "hex");
+ return params;
+}
+
+async function test(params) {
+ let digest = await crypto.subtle.digest(params.name, params.data);
+ digest = Buffer.from(digest).toString("hex");
+
+ if (params.expected != digest) {
+ throw Error(`${params.name} digest failed expected: "${params.expected}" vs "${digest}"`);
+ }
+
+ return 'SUCCESS';
+}
+
+let digest_tsuite = {
+ name: "SHA digest",
+ opts: { },
+
+ tests: [
+ { name: "XXX", data: "",
+ exception: "TypeError: unknown hash name: \"XXX\"" },
+ { name: "SHA-256", data: "",
+ expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" },
+ { name: "SHA-256", data: "aabbccdd",
+ expected: "8d70d691c822d55638b6e7fd54cd94170c87d19eb1f628b757506ede5688d297" },
+ { name: "SHA-256", data: "aabbccdd".repeat(4096),
+ expected: "25077ac2e5ba760f015ef34b93bc2b4682b6b48a94d65e21aaf2c8a3a62f6368" },
+ { name: "SHA-384", data: "",
+ expected: "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" },
+ { name: "SHA-384", data: "aabbccdd",
+ expected: "f9616ef3495efbae2f6af1a754620f3034487e9c60f3a9ef8138b5ed55cdd8d18ad9565653a5d68f678bd34cfa6f4490" },
+ { name: "SHA-384", data: "aabbccdd".repeat(4096),
+ expected: "50502d6e89bc34ecc826e0d56ccba0e010eff7b2b532e3bd627f4c828f6c741bf518fc834559360ccf7770f1b4d655d8" },
+ { name: "SHA-512", data: "",
+ expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" },
+ { name: "SHA-512", data: "aabbccdd",
+ expected: "48e218b30d4ea16305096fe35e84002a0d262eb3853131309423492228980c60238f9eed238285036f22e37c4662e40c80a461000a7aa9a03fb3cb6e4223e83b" },
+ { name: "SHA-512", data: "aabbccdd".repeat(4096),
+ expected: "9fcd0bd297646e207a2d655feb4ed4473e07ff24560a1e180a5eb2a67824f68affd9c7b5a8f747b9c39201f5f86a0085bb636c6fc34c216d9c10b4d728be096a" },
+ { name: "SHA-1", data: "",
+ expected: "da39a3ee5e6b4b0d3255bfef95601890afd80709" },
+ { name: "SHA-1", data: "aabbccdd",
+ expected: "a7b7e9592daa0896db0517bf8ad53e56b1246923" },
+ { name: "SHA-1", data: "aabbccdd".repeat(4096),
+ expected: "cdea58919606ea9ae078f7595b192b84446f2189" },
+]};
+
+run([digest_tsuite], test, p);
diff --git a/test/webcrypto/ec.pkcs8 b/test/webcrypto/ec.pkcs8
new file mode 100644
index 0000000..9829794
--- /dev/null
+++ b/test/webcrypto/ec.pkcs8
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0/4a3QXaSTJ0
+JKbSUbieKTD1UFtr7i/2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiD
+D7ykz4XSmElZ3ODc5/+7jc9AAN1OH4aX1cUg+FOUHIhshKDOK94wu24y
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/ec.spki b/test/webcrypto/ec.spki
new file mode 100644
index 0000000..c9f6058
--- /dev/null
+++ b/test/webcrypto/ec.spki
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZI
+gw+8pM+F0phJWdzg3Of/u43PQADdTh+Gl9XFIPhTlByIbISgziveMLtuMg==
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/ec2.pkcs8 b/test/webcrypto/ec2.pkcs8
new file mode 100644
index 0000000..b835530
--- /dev/null
+++ b/test/webcrypto/ec2.pkcs8
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg56d4aW5UtAvpKMfr
+E0M8OeCN/6ES0Q1Y+DeymtgvZ2ihRANCAATj283yk3EezOOEF6FRRwfeYNyJ65bj
+1jwJ8w9N0zMIedRGg0OJHnNc/uoyu6s1M/BtG/vZJ8IJNHUayiVbqxVL
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/ec2.spki b/test/webcrypto/ec2.spki
new file mode 100644
index 0000000..afde080
--- /dev/null
+++ b/test/webcrypto/ec2.spki
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49vN8pNxHszjhBehUUcH3mDcieuW
+49Y8CfMPTdMzCHnURoNDiR5zXP7qMrurNTPwbRv72SfCCTR1GsolW6sVSw==
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa.js b/test/webcrypto/rsa.js
new file mode 100644
index 0000000..36744d8
--- /dev/null
+++ b/test/webcrypto/rsa.js
@@ -0,0 +1,106 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function pem_to_der(pem, type) {
+ const pemJoined = pem.toString().split('\n').join('');
+ const pemHeader = `-----BEGIN ${type} KEY-----`;
+ const pemFooter = `-----END ${type} KEY-----`;
+ const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+ return Buffer.from(pemContents, 'base64');
+}
+
+function p(args, default_opts) {
+ let params = Object.assign({}, default_opts, args);
+
+ params.data = Buffer.from(params.data, "hex");
+
+ return params;
+}
+
+async function test(params) {
+ let spki = await crypto.subtle.importKey("spki",
+ pem_to_der(fs.readFileSync(`test/webcrypto/${params.spki}`), "PUBLIC"),
+ {name:"RSA-OAEP", hash:params.spki_hash},
+ false, ["encrypt"]);
+
+ let pkcs8 = await crypto.subtle.importKey("pkcs8",
+ pem_to_der(fs.readFileSync(`test/webcrypto/${params.pkcs8}`), "PRIVATE"),
+ {name:"RSA-OAEP", hash:params.pkcs8_hash},
+ false, ["decrypt"]);
+
+ let enc = await crypto.subtle.encrypt({name: "RSA-OAEP"}, spki, params.data);
+
+ let plaintext = await crypto.subtle.decrypt({name: "RSA-OAEP"}, pkcs8, enc);
+
+ plaintext = Buffer.from(plaintext);
+
+ if (params.data.compare(plaintext) != 0) {
+ throw Error(`RSA-OAEP encoding/decoding failed expected: "${params.data}" vs "${plaintext}"`);
+ }
+
+ return 'SUCCESS';
+};
+
+let rsa_tsuite = {
+ name: "RSA-OAEP encoding/decoding",
+ opts: {
+ spki: "rsa.spki",
+ spki_hash: "SHA-256",
+ pkcs8: "rsa.pkcs8",
+ pkcs8_hash: "SHA-256",
+ },
+
+ tests: [
+ { data: "aabbcc" },
+ { data: "aabbccdd".repeat(4) },
+ { data: "aabbccdd".repeat(7) },
+ { data: "aabbcc", spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+ { data: "aabbccdd".repeat(4), spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+ { data: "aabbccdd".repeat(7), spki_hash: "SHA-1", pkcs8_hash: "SHA-1" },
+ { data: "aabbcc", spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+ { data: "aabbccdd".repeat(4), spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+ { data: "aabbccdd".repeat(7), spki_hash: "SHA-384", pkcs8_hash: "SHA-384" },
+
+ { data: "aabbcc", spki_hash: "SHA-256", pkcs8_hash: "SHA-384", exception: "Error: EVP_PKEY_decrypt() failed" },
+ { data: "aabbcc", spki_hash: "XXX", exception: "TypeError: unknown hash name: \"XXX\"" },
+ { data: "aabbcc", spki: "rsa.spki.broken", exception: "Error: d2i_PUBKEY() failed" },
+ { data: "aabbcc", spki: "rsa2.spki", exception: "Error: EVP_PKEY_decrypt() failed" },
+]};
+
+run([rsa_tsuite], test, p);
diff --git a/test/webcrypto/rsa.pkcs8 b/test/webcrypto/rsa.pkcs8
new file mode 100644
index 0000000..0065b98
--- /dev/null
+++ b/test/webcrypto/rsa.pkcs8
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMlJsaCQvFQDOYcm
+GWvl1AWYNTdcsBTD1KVrBdZGkhnnffD911ID84F/NMKcs3eanRrgC6p39pTHOzvD
+6xgbTuWK70JSPejV9I1KOW3OcM9ttKG9wFAnkJ038flBajOKQsI6A0qNj5aYSXVo
+BWMphgWgQiYJxDUC/R9Tf/P8jYjfAgMBAAECgYEAj06DQyCopFujYoASi0oWmGEU
+SjUYO8BsrdSzVCnsLLsuZBwlZ4Peouyw4Hl2IIoYniCyzYwZJzVtC5Dh2MjgcrJT
+G5nX3FfheuabGl4in0583C51ZYWlVpDvBWw8kJTfXjiKH4z6ZA9dWdT5Y3aH/kOf
++znUc7eTvuzISs61x/kCQQD0BJvbLDlvx3u6esW47LLgQNw9ufMSlu5UYBJ4c+qQ
+5HAeyp4Zt/AaWENhJitjQcLBSxIFIVw7dIN67RnTNK8VAkEA0yvzzgHo/PGYSlVj
++M3965AwQF2wTXz82MZHv6EfcCHKuBfCSecr+igqLHhzfynAQjjf39VrXuPuRL23
+REF1IwJBAKVFydo0peJTljXDmc+aYb0JsSINo9jfaSS0vU3gFOt2DYqNaW+56WGu
+jlRqadCcZbBNjDL1WWbbj4HevTMT59ECQEWaKgzPolykwN5XUNE0DCp1ZwIAH1kb
+Bjfo+sMVt0f9S1TsN9SmBl+4l1X7CY5zU3RATMH5FR+8ns83fM1ZieMCQQDZEQ+d
+FAhouzJrnCXAXDTCHA9oBtNmnaN+C6G2DmCi79iu7sLHP9vzdgU+CgjrG4YTU5ex
+aRFNOhLwW4hYKs0F
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa.pkcs8.broken b/test/webcrypto/rsa.pkcs8.broken
new file mode 100644
index 0000000..6341afe
--- /dev/null
+++ b/test/webcrypto/rsa.pkcs8.broken
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMlJsaCQvFQDOYcm
+GWvl1AWYNTdcsBTD1KVrBdZGkhnnffD911ID84F/NMKcs3eanRrgC6p39pTHOzvD
+6xgbTuWK70JSPejV9I1KOW3OcM9ttKG9wFAnkJ038flBajOKQsI6A0qNj5aYSXVo
+BWMphgWgQiYJxDUC/R9Tf/P8jYjfAgMBAAECgYEAj06DQyCopFujYoASi0oWmGEU
+SjUYO8BsrdSzVCnsLLsuZBwlZ4Peouyw4Hl2IIoYniCyzYwZJzVtC5Dh2MjgcrJT
+G5nX3FfheuabGl4in0583C51ZYWlVpDvBWw8kJTfXjiKH4z6ZA9dWdT5Y3aH/kOf
++znUc7eTvuzISs61x/kCQQD0BJvbLDlvx3u6esW47LLgQNw9ufMSlu5UYBJ4c+qQ
+5HAeyp4Zt/AaWENhJitjQcLBSxIFIVw7dIN67RnTNK8VAkEA0yvzzgHo/PGYSlVj
++M3965AwQF2wTXz82MZHv6EfcCHKuBfCSecr+igqLHhzfynAQjjf39VrXuPuRL23
+REF1IwJBAKVFydo0peJTljXDmc+aYb0JsSINo9jfaSS0vU3gFOt2DYqNaW+56WGu
+jlRqadCcZbBNjDL1WWbbj4HevTMT59ECQEWaKgzPolykwN5XUNE0DCp1ZwIAH1kb
+Bjfo+sMVt0f9S1TsN9SmBl+4l1X7CY5zU3RATMH5FR+8ns83fM1ZieMCQQDZEQ+d
+FAhouzJrnCXAXDTCHA9oBtNmnaN+C6G2DmCi79iu7sLHP9vzdgU+CgjrG4YTU5ex
+aRFNOhLwW4hYKs
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa.spki b/test/webcrypto/rsa.spki
new file mode 100644
index 0000000..6ff75cf
--- /dev/null
+++ b/test/webcrypto/rsa.spki
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJSbGgkLxUAzmHJhlr5dQFmDU3
+XLAUw9SlawXWRpIZ533w/ddSA/OBfzTCnLN3mp0a4Auqd/aUxzs7w+sYG07liu9C
+Uj3o1fSNSjltznDPbbShvcBQJ5CdN/H5QWozikLCOgNKjY+WmEl1aAVjKYYFoEIm
+CcQ1Av0fU3/z/I2I3wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa.spki.broken b/test/webcrypto/rsa.spki.broken
new file mode 100644
index 0000000..d3f35d8
--- /dev/null
+++ b/test/webcrypto/rsa.spki.broken
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJSbGgkLxUAzmHJhlr5dQFmDU3
+XLAUw9SlawXWRpIZ533w/ddSA/OBfzTCnLN3mp0a4Auqd/aUxzs7w+sYG07liu9C
+Uj3o1fSNSjltznDPbbShvcBQJ5CdN/H5QWozikLCOgNKjY+WmEl1aAVjKYYFoEIm
+CcQ1Av0fU3/z/I2I3IDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa2.pkcs8 b/test/webcrypto/rsa2.pkcs8
new file mode 100644
index 0000000..2520b2d
--- /dev/null
+++ b/test/webcrypto/rsa2.pkcs8
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANUukQQW3CVlMsS6
+VaRnBhExg2gTCB6tvevFupCpq6sEuX5X6+4/QUTt4/wyWeMn5amaNAjGmDtsmeKX
+85MGHPuKceLhjToJT5JGFFDwVB6pNyqBuRzEHYsvQz1TlqcSdo7U/YaQtqkEZsA/
+ZAjBSR4lP9ggOBpEq+bBs+2GaZZFAgMBAAECgYB8HMdK1TBICTncdQtlUqGiouv5
+TJM+oTJgMNbkYBPU1kRUPUXbiDIsuj8wVfQlHtZDvsYqkcyRVDHnTUX+w+FctIox
+OU+3bKEZ/winaO3znvPVdy59/evpvQ0rnAGsxYBmyfZTqQgCxX+nMqAsLIplzORM
+5zAOawEQfRyHGETHQQJBAPe6YoMHMM/ZVpQ0xwQasbQahcL2GCD4Hwv3neGEKZVz
+Aos89/qkA+78hg6OxChxSJFxK0p35lu5TqF0QFX3MNUCQQDcTOBFZaGMHnnL+2uc
+tTmjKMjt47Es5G/NLg5z4cLBeeaz2St8ISbtvuVtl3K9cjeNy4J30zvF5puZrTvw
+/wexAkEAsfzRcNEGyh+erCdrYlCHox53QsesOGvtapzDa9eYRQ94IXBxvzx+swPu
+kaET4Pbbq9wCvaN9+CMhErHC08Eh7QJAAQWaRLgj97JsfjW8Wg29JrSZugDEYaDt
+o9YC2ybA8ITQPSVUvk6pD5FDHy8EqTxOZan8APJJ5LEdJ6lWDdghAQJBANKmYYmk
+OcQtU29dwuzPkwZWFdl6mhwZdcOrFcjq2pSfxKjBfygXXykscF8pHeQsjPrKK3u6
+HNED24fqNlbYHi8=
+-----END PRIVATE KEY-----
diff --git a/test/webcrypto/rsa2.spki b/test/webcrypto/rsa2.spki
new file mode 100644
index 0000000..fde2938
--- /dev/null
+++ b/test/webcrypto/rsa2.spki
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVLpEEFtwlZTLEulWkZwYRMYNo
+Ewgerb3rxbqQqaurBLl+V+vuP0FE7eP8MlnjJ+WpmjQIxpg7bJnil/OTBhz7inHi
+4Y06CU+SRhRQ8FQeqTcqgbkcxB2LL0M9U5anEnaO1P2GkLapBGbAP2QIwUkeJT/Y
+IDgaRKvmwbPthmmWRQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/test/webcrypto/rsa_decoding.js b/test/webcrypto/rsa_decoding.js
new file mode 100644
index 0000000..c5e0f65
--- /dev/null
+++ b/test/webcrypto/rsa_decoding.js
@@ -0,0 +1,81 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function pem_to_der(pem) {
+ const pemJoined = pem.toString().split('\n').join('');
+ const pemHeader = '-----BEGIN PRIVATE KEY-----';
+ const pemFooter = '-----END PRIVATE KEY-----';
+ const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+ return Buffer.from(pemContents, 'base64');
+}
+
+function base64decode(b64) {
+ const joined = b64.toString().split('\n').join('');
+ return Buffer.from(joined, 'base64');
+}
+
+async function test(params) {
+ let pem = fs.readFileSync(`test/webcrypto/${params.pem}`);
+ let enc = base64decode(fs.readFileSync(`test/webcrypto/${params.src}`));
+
+ let key = await crypto.subtle.importKey("pkcs8", pem_to_der(pem),
+ {name:"RSA-OAEP", hash:"SHA-1"},
+ false, ["decrypt"]);
+
+ let plaintext = await crypto.subtle.decrypt({name: "RSA-OAEP"}, key, enc);
+ plaintext = new TextDecoder().decode(plaintext);
+
+ if (params.expected != plaintext) {
+ throw Error(`RSA-OAEP decoding failed expected: "${params.expected}" vs "${plaintext}"`);
+ }
+
+ return "SUCCESS";
+}
+
+let rsa_tsuite = {
+ name: "RSA-OAEP decoding",
+ opts: { },
+
+ tests: [
+ { pem: "rsa.pkcs8", src: "text.base64.rsa-oaep.enc", expected: "WAKAWAKA" },
+ { pem: "ec.pkcs8", src: "text.base64.rsa-oaep.enc", exception: "Error: RSA key is not found" },
+ { pem: "rsa.pkcs8.broken", src: "text.base64.rsa-oaep.enc", exception: "Error: d2i_PKCS8_PRIV_KEY_INFO_bio() failed" },
+]};
+
+run([rsa_tsuite], test, (v) => v);
diff --git a/test/webcrypto/sign.js b/test/webcrypto/sign.js
new file mode 100644
index 0000000..0473d2a
--- /dev/null
+++ b/test/webcrypto/sign.js
@@ -0,0 +1,282 @@
+const fs = require('fs');
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function merge(to, from) {
+ let r = Object.assign({}, to);
+ Object.keys(from).forEach(v => {
+ if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+ r[v] = merge(r[v], from[v]);
+
+ } else if (typeof from[v] == 'object') {
+ r[v] = Object.assign({}, from[v]);
+
+ } else {
+ r[v] = from[v];
+ }
+ })
+
+ return r;
+};
+
+function pem_to_der(pem, type) {
+ const pemJoined = pem.toString().split('\n').join('');
+ const pemHeader = `-----BEGIN ${type} KEY-----`;
+ const pemFooter = `-----END ${type} KEY-----`;
+ const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+ return Buffer.from(pemContents, 'base64');
+}
+
+function base64decode(b64) {
+ const joined = b64.toString().split('\n').join('');
+ return Buffer.from(joined, 'base64');
+}
+
+function p(args, default_opts) {
+ let key;
+ let encoder = new TextEncoder();
+ let params = merge({}, default_opts);
+ params = merge(params, args);
+
+ switch (params.sign_key.fmt) {
+ case "pkcs8":
+ let pem = fs.readFileSync(`test/webcrypto/${params.sign_key.key}`);
+ key = pem_to_der(pem, "PRIVATE");
+ break;
+ case "raw":
+ key = encoder.encode(params.sign_key.key);
+ break;
+ default:
+ throw Error("Unknown sign key format");
+ }
+
+ params.sign_key.key = key;
+
+ switch (params.verify_key.fmt) {
+ case "spki":
+ let pem = fs.readFileSync(`test/webcrypto/${params.verify_key.key}`);
+ key = pem_to_der(pem, "PUBLIC");
+ break;
+ case "raw":
+ key = encoder.encode(params.verify_key.key);
+ break;
+ default:
+ throw Error("Unknown verify key format");
+ }
+
+ params.verify_key.key = key;
+
+ return params;
+}
+
+async function test(params) {
+ let encoder = new TextEncoder();
+ let sign_key = await crypto.subtle.importKey(params.sign_key.fmt,
+ params.sign_key.key,
+ params.import_alg,
+ false, [ "sign" ]);
+
+ let sig = await crypto.subtle.sign(params.sign_alg, sign_key,
+ encoder.encode(params.text));
+
+ if (params.verify) {
+ let verify_key = await crypto.subtle.importKey(params.verify_key.fmt,
+ params.verify_key.key,
+ params.import_alg,
+ false, [ "verify" ]);
+
+ let r = await crypto.subtle.verify(params.sign_alg, verify_key, sig,
+ encoder.encode(params.text));
+
+ if (params.expected !== r) {
+ throw Error(`${params.sign_alg.name} failed expected: "${params.expected}" vs "${r}"`);
+ }
+
+ if (params.expected === true) {
+ let broken_sig = Buffer.concat([Buffer.from(sig)]);
+ broken_sig[8] = 255 - broken_sig[8];
+
+ r = await crypto.subtle.verify(params.sign_alg, verify_key, broken_sig,
+ encoder.encode(params.text));
+ if (r !== false) {
+ throw Error(`${params.sign_alg.name} BROKEN SIG failed expected: "false" vs "${r}"`);
+ }
+
+ let broken_text = encoder.encode(params.text);
+ broken_text[0] = 255 - broken_text[0];
+
+ r = await crypto.subtle.verify(params.sign_alg, verify_key, sig,
+ broken_text);
+ if (r !== false) {
+ throw Error(`${params.sign_alg.name} BROKEN TEXT failed expected: "false" vs "${r}"`);
+ }
+ }
+
+ } else {
+ sig = Buffer.from(sig).toString("hex");
+
+ if (params.expected !== sig) {
+ throw Error(`${params.sign_alg.name} failed expected: "${params.expected}" vs "${sig}"`);
+ }
+ }
+
+
+ return "SUCCESS";
+}
+
+let hmac_tsuite = {
+ name: "HMAC sign",
+ opts: {
+ text: "TExt-T0-SiGN",
+ sign_key: { key: "secretKEY", fmt: "raw" },
+ verify_key: { key: "secretKEY", fmt: "raw" },
+ verify: false,
+ import_alg: {
+ name: "HMAC",
+ hash: "SHA-256",
+ },
+ sign_alg: {
+ name: "HMAC",
+ },
+ },
+
+ tests: [
+ { expected: "76d4f1b22d7544c34e86380c9ab7c756311810dc31e4af3b705045d263db1212" },
+ { import_alg: { hash: "SHA-384" },
+ expected: "4bdaa7e80868a9cda35ad78ae5d88c29f1ff97680317c5bc3df1deccf2dad0cf3edce945ed90ec53fa48d887a04d4963" },
+ { import_alg: { hash: "SHA-512" },
+ expected: "9dd589ae5e75b6fb8d453c072cc05e6f5eb3d29034d3a0df2559ffe158f3f99fef98a9d1ab2fca459cceea0be3cb7aa3269d77fc9382b56a9cd0571851339938" },
+ { import_alg: { hash: "SHA-1" },
+ expected: "0540c587e7ee607fb4fd5e814438ed50f261c244" },
+ { sign_alg: { name: "ECDSA" }, exception: "TypeError: cannot sign using \"HMAC\" with \"ECDSA\" key" },
+
+ { verify: true, expected: true },
+ { verify: true, import_alg: { hash: "SHA-384" }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+ { verify: true, verify_key: { key: "secretKEY2" }, expected: false },
+]};
+
+let rsassa_pkcs1_v1_5_tsuite = {
+ name: "RSASSA-PKCS1-v1_5 sign",
+ opts: {
+ text: "TExt-T0-SiGN",
+ sign_key: { key: "rsa.pkcs8", fmt: "pkcs8" },
+ verify_key: { key: "rsa.spki", fmt: "spki" },
+ import_alg: {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ },
+ sign_alg: {
+ name: "RSASSA-PKCS1-v1_5",
+ },
+ },
+
+ tests: [
+ { expected: "b126c528abd305dc2b7234de44ffa2190bd55f57087f75620196e8bdb05ba205e52ceca03e4799f30a6d61a6610878b1038a5dd869ab8c04ffe80d49d14407b2c2fe52ca78c9c409fcf7fee26188941f5072179c2bf2de43e637b089c32cf04f14ca01e7b9c33bbbec603b2815de0180b12a3269b0453aba158642e00303890d" },
+ { import_alg: { hash: "SHA-512" },
+ expected: "174adca014132f5b9871e1bda2c23fc50f57673c6915b9170d601c626022a03d66c1b8c2a4b8efa08edee83ad27cc05c0d33c7a52a9125fa5be0f99be40483d8123570f91d53f2af51ef0f2b43987182fd114db242f146ea0d7c4ead5d4a11043f83e67d5400fc66dc2b08d7d63122fcd11b495fb4115ecf57c51994f6c516b9" },
+ { import_alg: { hash: "SHA-1" },
+ expected: "0cc6377ae31a1b09a7c0a18d12e785e9734565bdeb808b3e41d8bc03adab9ffbd8b1764830fea8f1d8f327034f24296f3aad6112cc3a380db6ef01989f8f9cb608f75b1d9558c36785b6f932ee06729b139b5f02bb886fd1d4fb0f06246064993a421e55579c490c77c27a44c7cc0ea7dd6579cc69402177712ba0f69cac967d" },
+
+ { verify: true, expected: true },
+ { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+ { verify: true, verify_key: { key: "rsa2.spki" }, expected: false },
+]};
+
+let rsa_pss_tsuite = {
+ name: "RSA-PSS sign",
+ opts: {
+ text: "TExt-T0-SiGN",
+ sign_key: { key: "rsa.pkcs8", fmt: "pkcs8" },
+ verify_key: { key: "rsa.spki", fmt: "spki" },
+ import_alg: {
+ name: "RSA-PSS",
+ hash: "SHA-256",
+ },
+ sign_alg: {
+ name: "RSA-PSS",
+ saltLength: 0,
+ },
+ },
+
+ tests: [
+ { expected: "c126f05ea6e13b3208540bd833f5886d95fe2c89f9b3102b564c9da3bc0c00d224e6ed9be664dee61dfcc0eee790f816c5cf6a0ffc320112d818b72d57de9adbb31d239c225d42395c906bde719bf4ad21c18c679d70186d2efc044fc4995773c5085c64c6d9b7a5fc96dd28176e2cd702a9f35fe64b960f21523ec19bb44408" },
+ { import_alg: { hash: "SHA-512" }, expected: "3764287839843d25cb8ad109d0ffffd54a8f47fae02e9d2fa8a9363a7b0f98d0ede417c57c0d99a8c11cd502bbc95767a5f437b99cb30341c7af840889633e08cfdaae472bed3e68d451c67182ccd583457c6a9cf81c7e17fb391606f1bc02a83253975f153582ca1c31e9ba9b89dec4bf1d2a9b7b5024dd4dde317432ff26b1" },
+ { import_alg: { hash: "SHA-1" }, expected: "73d39d22b028b13142b257d405a4a09d0622b97ef7b74e0953274744a76fedee0f283b678cfcaa8e4c38ef84033259f84c59ae987f9d049adea4379a9b0addb9f8b53ee6b64a4e32d8165d057444a1056706da648b88c6a4613022e03be5b6b9e8948d9527a95478f871bfe88dbc67127b038520af3400b942c85e0733bcad27" },
+
+ { verify: true, expected: true },
+ { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+ { verify: true, sign_alg: { saltLength: 32 }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-512" }, sign_alg: { saltLength: 32 },
+ expected: true },
+ { verify: true, verify_key: { key: "rsa2.spki" }, expected: false },
+]};
+
+let ecdsa_tsuite = {
+ name: "ECDSA sign",
+ opts: {
+ text: "TExt-T0-SiGN",
+ sign_key: { key: "ec.pkcs8", fmt: "pkcs8" },
+ verify_key: { key: "ec.spki", fmt: "spki" },
+ import_alg: {
+ name: "ECDSA",
+ namedCurve: "P-256",
+ },
+ sign_alg: {
+ name: "ECDSA",
+ hash: "SHA-256",
+ },
+ },
+
+ tests: [
+ { verify: true, expected: true },
+ { verify: true, import_alg: { hash: "SHA-384" }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-512" }, expected: true },
+ { verify: true, import_alg: { hash: "SHA-1" }, expected: true },
+ { verify: true, verify_key: { key: "ec2.spki" }, expected: false },
+ { verify: true, verify_key: { key: "rsa.spki" }, exception: "Error: EC key is not found" },
+ { verify: true, import_alg: { namedCurve: "P-384" }, exception: "Error: name curve mismatch" },
+]};
+
+run([
+ hmac_tsuite,
+ rsassa_pkcs1_v1_5_tsuite,
+ rsa_pss_tsuite,
+ ecdsa_tsuite
+], test, p);
diff --git a/test/webcrypto/text.base64.aes-cbc128.enc b/test/webcrypto/text.base64.aes-cbc128.enc
new file mode 100644
index 0000000..8ee6ad6
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-cbc128.enc
@@ -0,0 +1 @@
+pKzTDFjJuyyWxBpM0++pVETg9638AXJwa9yXCL3Av0c=
diff --git a/test/webcrypto/text.base64.aes-cbc256.enc b/test/webcrypto/text.base64.aes-cbc256.enc
new file mode 100644
index 0000000..b4b623f
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-cbc256.enc
@@ -0,0 +1 @@
+3gnuDWCYtwPW5TMPtj1LM/uJxnKknbvPn9gURBEcegE=
diff --git a/test/webcrypto/text.base64.aes-ctr128.enc b/test/webcrypto/text.base64.aes-ctr128.enc
new file mode 100644
index 0000000..4ed5331
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-ctr128.enc
@@ -0,0 +1 @@
+UsVG2TjNHGbXaTZ3fG67MsxXPw==
diff --git a/test/webcrypto/text.base64.aes-ctr256.enc b/test/webcrypto/text.base64.aes-ctr256.enc
new file mode 100644
index 0000000..f1b4a41
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-ctr256.enc
@@ -0,0 +1 @@
+jnIqHDDRajcKkHwo4IormMSsBSDEI40=
diff --git a/test/webcrypto/text.base64.aes-gcm128-96.enc b/test/webcrypto/text.base64.aes-gcm128-96.enc
new file mode 100644
index 0000000..64724bb
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128-96.enc
@@ -0,0 +1 @@
+z4NZNzf3eauJvuFQTopsTxPSERfT4lpbnK1ILuqw81OBxqw8cpheqTfXi7U5
diff --git a/test/webcrypto/text.base64.aes-gcm128-extra.enc b/test/webcrypto/text.base64.aes-gcm128-extra.enc
new file mode 100644
index 0000000..4bb99b4
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128-extra.enc
@@ -0,0 +1 @@
+z4NZNzf3eavxzIhNW4QOTRfQewfam0gzjLpOKIKwm1+QJfR0ElIvNEPnKHx4d+OxJMpT
diff --git a/test/webcrypto/text.base64.aes-gcm128.enc b/test/webcrypto/text.base64.aes-gcm128.enc
new file mode 100644
index 0000000..8b497d8
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm128.enc
@@ -0,0 +1 @@
+z4NZNzf3eavjzY9WSplsVxPEAuftE7KpHQoIoS+yI/lPxDk=
diff --git a/test/webcrypto/text.base64.aes-gcm256.enc b/test/webcrypto/text.base64.aes-gcm256.enc
new file mode 100644
index 0000000..7bcbb12
--- /dev/null
+++ b/test/webcrypto/text.base64.aes-gcm256.enc
@@ -0,0 +1 @@
+JCRgSCD1e7h0ogveLrzbaUBby151RIajzxFhHyD4JpD36kBOL2Kz
diff --git a/test/webcrypto/text.base64.rsa-oaep.enc b/test/webcrypto/text.base64.rsa-oaep.enc
new file mode 100644
index 0000000..829f059
--- /dev/null
+++ b/test/webcrypto/text.base64.rsa-oaep.enc
@@ -0,0 +1,3 @@
+lRLk3t6LYvQBJkOYqWYSWYcHaPmsskb+vgcV3bwnfHF2MNp5oALe14mn/4m759oWCQIgXA/3kC1E
+kHXOBERS2+wKOXD2hS68kZnnrfWq6//7yw7Fvzv9OjG5mYSUaKcO/p+zaJmorsgvOy+nyZJs+BPD
+dKU3ohuz1MJ0wrGkki4=
diff --git a/test/webcrypto/text.base64.sha1.ecdsa.sig b/test/webcrypto/text.base64.sha1.ecdsa.sig
new file mode 100644
index 0000000..be59cc2
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.ecdsa.sig
@@ -0,0 +1,2 @@
+MEQCIAZ/sGPfuYivvm5UsqZgiR2jtT88d2moIgnAh6h1jKdVAiALKiu3myhI046rhEThSLyReuTu
+eIEgeCPBa2xGZnFXEg==
diff --git a/test/webcrypto/text.base64.sha1.hmac.sig b/test/webcrypto/text.base64.sha1.hmac.sig
new file mode 100644
index 0000000..2515963
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.hmac.sig
@@ -0,0 +1 @@
+eVw25ESkzl+mDQs7z5VGkxqneZ4=
diff --git a/test/webcrypto/text.base64.sha1.pkcs1.sig b/test/webcrypto/text.base64.sha1.pkcs1.sig
new file mode 100644
index 0000000..95ba1e5
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.pkcs1.sig
@@ -0,0 +1,3 @@
+H4lVoQebJkYFFJyXwBT2C6QDJ2OUQhQ153WjnOzaXLtlUtHdI7EOv8/hJ84ojDRJ4IyLXtGO8up9
+3WUIPw1tfwAI3X36MbMN04+HKzVabg4cTy0HnFu3k7D2hq+1vn6rT1Q7xT9C2SJBFmR/HxC2oHKz
+NcpELOP8crsoqu0c3QY=
diff --git a/test/webcrypto/text.base64.sha1.rsa-pss.16.sig b/test/webcrypto/text.base64.sha1.rsa-pss.16.sig
new file mode 100644
index 0000000..0587647
--- /dev/null
+++ b/test/webcrypto/text.base64.sha1.rsa-pss.16.sig
@@ -0,0 +1,3 @@
+aHWhiEOYGTRZyJNeNoEELFFyN+ZYgFLI+rNyuuaQpLEWDHqbUY0tdGenvIiiUxN0GKq/72g6CvyH
+RXq9VUL4Q+qkDbmROzBC0/P+RqgxpcNVJQx04RyGRVnw+l+GzE3rwbDCQG+95okBOxnac21thk/k
+GBJQGXhimg6XkCK3vVo=
diff --git a/test/webcrypto/text.base64.sha256.ecdsa.sig b/test/webcrypto/text.base64.sha256.ecdsa.sig
new file mode 100644
index 0000000..c7e72be
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.ecdsa.sig
@@ -0,0 +1,2 @@
+MEUCIFEw11evEWohKswRe3Za0P0u7mvGj4kSnHix/EOKhxApAiEAq2QtwNvFg8RdY6t01ff8mUTP
+nT1lEfMSRZmtuVxQuQA=
diff --git a/test/webcrypto/text.base64.sha256.hmac.sig b/test/webcrypto/text.base64.sha256.hmac.sig
new file mode 100644
index 0000000..130cc62
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.hmac.sig
@@ -0,0 +1 @@
+UbsmYPe0ek53QMpDPP/Y4V50bRXQQGNrYCTuzg1tznE=
diff --git a/test/webcrypto/text.base64.sha256.hmac.sig.broken b/test/webcrypto/text.base64.sha256.hmac.sig.broken
new file mode 100644
index 0000000..f2b59c0
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.hmac.sig.broken
@@ -0,0 +1 @@
+UbsmYPe0ek53QMpDPP/Y4V50bRXQQGNrYCAuzg1tznE=
diff --git a/test/webcrypto/text.base64.sha256.pkcs1.sig b/test/webcrypto/text.base64.sha256.pkcs1.sig
new file mode 100644
index 0000000..078d42d
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.pkcs1.sig
@@ -0,0 +1,3 @@
+IyGM/e2IMJCrJmE91GkddTyCble3554d8KvpbL1k10QrRlDE1afCab7iwmz4j1yl8pNMbSTKIb0y
+RfMQ3YlsKxzoJm2+pIrXErb2jF3emnVkUxNuSY3/ROK3rU8YirPbKhvkjulVwVlh4b6YpiXwuKTL
+HDmHp7AOr7yzjS3VZrs=
diff --git a/test/webcrypto/text.base64.sha256.rsa-pss.0.sig b/test/webcrypto/text.base64.sha256.rsa-pss.0.sig
new file mode 100644
index 0000000..ef84420
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.rsa-pss.0.sig
@@ -0,0 +1,3 @@
+DNQrIYaW4opZG1OdyFujH2rxgk06HB2eTUuCGyiN971pAVxCqYn0NhL7iMBrUrgsxnqBH+nNC1jg
+AMFGe0rtJE/9blWb9QiNz/kwitFI4oztXkcCHcQYwatbQTGQgqeA2rY9N6w6QwMAYJeEd4Jm0lTE
+oJx9N+C5QoArKBPgXxw=
diff --git a/test/webcrypto/text.base64.sha256.rsa-pss.32.sig b/test/webcrypto/text.base64.sha256.rsa-pss.32.sig
new file mode 100644
index 0000000..184bcf8
--- /dev/null
+++ b/test/webcrypto/text.base64.sha256.rsa-pss.32.sig
@@ -0,0 +1,3 @@
+B+69Fvykn5ncPxA91DQv7K5r5D25f0LuEK60h4WNWev8Hl/Bzseq315o/Ja7RhlgtYltBZTETqUk
+hkaWEWExCG1kw+xUbsz1HsdHJOwF+e52zirKGifondHVqTOl95aU5msRcuKtCEifnvgKqNE9c+Dz
+l1hoPlgGrhtnyI0DlcM=
diff --git a/test/webcrypto/verify.js b/test/webcrypto/verify.js
new file mode 100644
index 0000000..a969e3b
--- /dev/null
+++ b/test/webcrypto/verify.js
@@ -0,0 +1,207 @@
+const fs = require('fs');
+
+if (typeof crypto == 'undefined') {
+ crypto = require('crypto').webcrypto;
+}
+
+async function run(tlist, T, prepare_args) {
+ function validate(t, r, i) {
+ if (r.status == "fulfilled" && !t[i].exception) {
+ return r.value === "SUCCESS";
+ }
+
+ if (r.status == "rejected" && t[i].exception) {
+ if (process.argv[2] === '--match-exception-text') {
+ /* is not compatible with node.js format */
+ return r.reason.toString().startsWith(t[i].exception);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ for (let k = 0; k < tlist.length; k++) {
+ let ts = tlist[k];
+ let results = await Promise.allSettled(ts.tests.map(t => T(prepare_args(t, ts.opts))));
+ let r = results.map((r, i) => validate(ts.tests, r, i));
+
+ console.log(`${ts.name} ${r.every(v=>v == true) ? "SUCCESS" : "FAILED"}`);
+
+ r.forEach((v, i) => {
+ if (!v) {
+ console.log(`FAILED ${i}: ${JSON.stringify(ts.tests[i])}\n with reason: ${results[i].reason}`);
+ }
+ })
+ }
+}
+
+function merge(to, from) {
+ let r = Object.assign({}, to);
+ Object.keys(from).forEach(v => {
+ if (typeof r[v] == 'object' && typeof from[v] == 'object') {
+ r[v] = merge(r[v], from[v]);
+
+ } else if (typeof from[v] == 'object') {
+ r[v] = Object.assign({}, from[v]);
+
+ } else {
+ r[v] = from[v];
+ }
+ })
+
+ return r;
+};
+
+function base64decode(b64) {
+ const joined = b64.toString().split('\n').join('');
+ return Buffer.from(joined, 'base64');
+}
+
+function pem_to_der(pem, type) {
+ const pemJoined = pem.toString().split('\n').join('');
+ const pemHeader = `-----BEGIN ${type} KEY-----`;
+ const pemFooter = `-----END ${type} KEY-----`;
+ const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+ return Buffer.from(pemContents, 'base64');
+}
+
+function p(args, default_opts) {
+ let encoder = new TextEncoder();
+ let params = merge({}, default_opts);
+ params = merge(params, args);
+
+ switch (params.key.fmt) {
+ case "spki":
+ let pem = fs.readFileSync(`test/webcrypto/${params.key.file}`);
+ params.key.file = pem_to_der(pem, "PUBLIC");
+ break;
+ case "raw":
+ params.key.file = Buffer.from(params.key.file, "hex");
+ break;
+ }
+
+ params.signature = base64decode(fs.readFileSync(`test/webcrypto/${params.signature}`));
+ params.text = encoder.encode(params.text);
+
+ return params;
+}
+
+
+async function test(params) {
+ let key = await crypto.subtle.importKey(params.key.fmt,
+ params.key.file,
+ params.import_alg,
+ false, ["verify"]);
+
+ let r = await crypto.subtle.verify(params.verify_alg,
+ key, params.signature,
+ params.text);
+
+ if (params.expected !== r) {
+ throw Error(`${params.import_alg.name} failed expected: "${params.expected}" vs "${r}"`);
+ }
+
+ return 'SUCCESS';
+}
+
+let hmac_tsuite = {
+ name: "HMAC verify",
+ opts: {
+ text: "SigneD-TExt",
+ key: { fmt: "raw", file: "aabbcc" },
+ import_alg: {
+ name: "HMAC",
+ hash: "SHA-256",
+ },
+ verify_alg: {
+ name: "HMAC",
+ },
+ },
+
+ tests: [
+ { signature: "text.base64.sha256.hmac.sig", expected: true },
+ { signature: "text.base64.sha256.hmac.sig.broken", expected: false },
+ { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha1.hmac.sig", expected: true },
+ { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha256.hmac.sig", expected: false },
+ { key: { file: "aabbccdd" }, signature: "text.base64.sha256.hmac.sig", expected: false },
+]};
+
+let rsassa_pkcs1_v1_5_tsuite = {
+ name: "RSASSA-PKCS1-v1_5 verify",
+ opts: {
+ text: "SigneD-TExt",
+ key: { fmt: "spki", file: "rsa.spki" },
+ import_alg: {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ },
+ verify_alg: {
+ name: "RSASSA-PKCS1-v1_5",
+ },
+ },
+
+ tests: [
+ { signature: "text.base64.sha256.pkcs1.sig", expected: true },
+ { text: "SigneD-TExt2", signature: "text.base64.sha256.pkcs1.sig", expected: false },
+ { signature: "text.base64.sha1.pkcs1.sig", expected: false },
+ { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha1.pkcs1.sig", expected: true },
+ { key: { file: "rsa2.spki"}, signature: "text.base64.sha256.pkcs1.sig", expected: false },
+]};
+
+let rsa_pss_tsuite = {
+ name: "RSA-PSS verify",
+ opts: {
+ text: "SigneD-TExt",
+ key: { fmt: "spki", file: "rsa.spki" },
+ import_alg: {
+ name: "RSA-PSS",
+ hash: "SHA-256",
+ },
+ verify_alg: {
+ name: "RSA-PSS",
+ saltLength: 32,
+ },
+ },
+
+ tests: [
+ { signature: "text.base64.sha256.rsa-pss.32.sig", expected: true },
+ { text: "SigneD-TExt2", signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+ { key: { file: "rsa2.spki"}, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+ { verify_alg: { saltLength: 0 }, signature: "text.base64.sha256.rsa-pss.0.sig", expected: true },
+ { verify_alg: { saltLength: 0 }, signature: "text.base64.sha256.rsa-pss.0.sig", expected: true },
+ { import_alg: { hash: "SHA-1" }, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+ { import_alg: { hash: "SHA-1" }, verify_alg: { saltLength: 16 }, signature: "text.base64.sha1.rsa-pss.16.sig",
+ expected: true },
+ { verify_alg: { saltLength: 16 }, signature: "text.base64.sha256.rsa-pss.32.sig", expected: false },
+]};
+
+let ecdsa_tsuite = {
+ name: "ECDSA verify",
+ opts: {
+ text: "SigneD-TExt",
+ key: { fmt: "spki", file: "ec.spki" },
+ import_alg: {
+ name: "ECDSA",
+ namedCurve: "P-256",
+ },
+ verify_alg: {
+ name: "ECDSA",
+ hash: "SHA-256",
+ },
+ },
+
+ tests: [
+ { signature: "text.base64.sha256.ecdsa.sig", expected: true },
+ { signature: "text.base64.sha1.ecdsa.sig", expected: false },
+ { verify_alg: { hash: "SHA-1"}, signature: "text.base64.sha1.ecdsa.sig", expected: true },
+ { key: { file: "ec2.spki" }, signature: "text.base64.sha256.ecdsa.sig", expected: false },
+]};
+
+run([
+ hmac_tsuite,
+ rsassa_pkcs1_v1_5_tsuite,
+ rsa_pss_tsuite,
+ ecdsa_tsuite,
+], test, p);
diff --git a/ts/index.d.ts b/ts/index.d.ts
index 8a324cc..eec5c34 100644
--- a/ts/index.d.ts
+++ b/ts/index.d.ts
@@ -1,4 +1,5 @@
/// <reference path="njs_core.d.ts" />
+/// <reference path="njs_webcrypto.d.ts" />
/// <reference path="njs_modules/crypto.d.ts" />
/// <reference path="njs_modules/fs.d.ts" />
/// <reference path="njs_modules/querystring.d.ts" />
diff --git a/ts/njs_core.d.ts b/ts/njs_core.d.ts
index 808c08f..7037845 100644
--- a/ts/njs_core.d.ts
+++ b/ts/njs_core.d.ts
@@ -584,7 +584,7 @@
writeFloatLE(value: number, offset?: number): number;
}
-type NjsStringOrBuffer = NjsStringLike | Buffer | DataView | TypedArray;
+type NjsStringOrBuffer = NjsStringLike | Buffer | DataView | TypedArray | ArrayBuffer;
// Global objects
diff --git a/ts/njs_webcrypto.d.ts b/ts/njs_webcrypto.d.ts
new file mode 100644
index 0000000..b67ee0d
--- /dev/null
+++ b/ts/njs_webcrypto.d.ts
@@ -0,0 +1,226 @@
+interface RsaOaepParams {
+ name: "RSA-OAEP";
+}
+
+interface AesCtrParams {
+ name: "AES-CTR";
+ counter: NjsStringOrBuffer;
+ length: number;
+}
+
+interface AesCbcParams {
+ name: "AES-CBC";
+ iv: NjsStringOrBuffer;
+}
+
+interface AesGcmParams {
+ name: "AES-GCM";
+ iv: NjsStringOrBuffer;
+ additionalData?: NjsStringOrBuffer;
+ tagLength?: number;
+}
+
+type CipherAlgorithm =
+ | RsaOaepParams
+ | AesCtrParams
+ | AesCbcParams
+ | AesCbcParams;
+
+type HashVariants = "SHA-256" | "SHA-384" | "SHA-512" | "SHA-1";
+
+interface RsaHashedImportParams {
+ name: "RSASSA-PKCS1-v1_5" | "RSA-PSS" | "RSA-OAEP";
+ hash: HashVariants;
+}
+
+interface EcKeyImportParams {
+ name: "ECDSA";
+ namedCurve: "P-256" | "P-384" | "P-521";
+}
+
+interface HmacImportParams {
+ name: "HMAC";
+ hash: HashVariants;
+}
+
+type AesVariants = "AES-CTR" | "AES-CBC" | "AES-GCM";
+
+interface AesImportParams {
+ name: AesVariants;
+}
+
+type ImportAlgorithm =
+ | RsaHashedImportParams
+ | EcKeyImportParams
+ | HmacImportParams
+ | AesImportParams
+ | AesVariants
+ | "PBKDF2"
+ | "HKDF";
+
+interface HkdfParams {
+ name: "HKDF";
+ hash: HashVariants;
+ salt: NjsStringOrBuffer;
+ info: NjsStringOrBuffer;
+}
+
+interface Pbkdf2Params {
+ name: "PBKDF2";
+ hash: HashVariants;
+ salt: NjsStringOrBuffer;
+ interations: number;
+}
+
+type DeriveAlgorithm =
+ | HkdfParams
+ | Pbkdf2Params;
+
+interface HmacKeyGenParams {
+ name: "HMAC";
+ hash: HashVariants;
+}
+
+interface AesKeyGenParams {
+ name: AesVariants;
+ length: number;
+}
+
+type DeriveKeyAlgorithm =
+ | HmacKeyGenParams
+ | AesKeyGenParams;
+
+interface RsaPssParams {
+ name: "RSA-PSS";
+ saltLength: number;
+}
+
+interface EcdsaParams {
+ name: "ECDSA";
+ hash: HashVariants;
+}
+
+type SignOrVerifyAlgorithm =
+ | RsaPssParams
+ | EcdsaParams
+ | { name: "HMAC"; }
+ | { name: "RSASSA-PKCS1-v1_5"; }
+ | "HMAC"
+ | "RSASSA-PKCS1-v1_5";
+
+interface CryptoKey {
+}
+
+interface SubtleCrypto {
+ /**
+ * Decrypts encrypted data.
+ *
+ * @param algorithm Object specifying the algorithm to be used,
+ * and any extra parameters as required.
+ * @param key CryptoKey containing the key to be used for decryption.
+ * @param data Data to be decrypted.
+ */
+ decrypt(algorithm: CipherAlgorithm,
+ key: CryptoKey,
+ data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+ /**
+ * Derives an array of bits from a base key.
+ *
+ * @param algorithm Object defining the derivation algorithm to use.
+ * @param baseKey CryptoKey representing the input to the derivation algorithm.
+ * @param length Number representing the number of bits to derive.
+ */
+ deriveBits(algorithm: DeriveAlgorithm,
+ baseKey: CryptoKey,
+ length: number): Promise<ArrayBuffer>;
+
+ /**
+ * Derives a secret key from a master key.
+ *
+ * @param algorithm Object defining the derivation algorithm to use.
+ * @param baseKey CryptoKey representing the input to the derivation algorithm.
+ * @param derivedKeyAlgorithm Object defining the algorithm the
+ * derived key will be used for.
+ * @param extractable Unsupported.
+ * @param usage Array indicating what can be done with the key.
+ * Possible array values: "encrypt", "decrypt", "sign", "verify",
+ * "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
+ */
+ deriveKey(algorithm: DeriveAlgorithm,
+ baseKey: CryptoKey,
+ derivedKeyAlgorithm: DeriveKeyAlgorithm,
+ extractable: boolean,
+ usage: Array<string>): Promise<CryptoKey>;
+
+ /**
+ * Generates a digest of the given data.
+ *
+ * @param algorithm String defining the hash function to use.
+ */
+ digest(algorithm: HashVariants,
+ data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+ /**
+ * Encrypts data.
+ *
+ * @param algorithm Object specifying the algorithm to be used,
+ * and any extra parameters as required.
+ * @param key CryptoKey containing the key to be used for encryption.
+ * @param data Data to be encrypted.
+ */
+ encrypt(algorithm: CipherAlgorithm,
+ key: CryptoKey,
+ data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+ /**
+ * Imports a key.
+ *
+ * @param format String describing the data format of the key to import.
+ * @param keyData Object containing the key in the given format.
+ * @param algorithm Dictionary object defining the type of key to import
+ * and providing extra algorithm-specific parameters.
+ * @param extractable Unsupported.
+ * @param usage Array indicating what can be done with the key.
+ * Possible array values: "encrypt", "decrypt", "sign", "verify",
+ * "deriveKey", "deriveBits", "wrapKey", "unwrapKey".
+ */
+ importKey(format: "raw" | "pkcs8" | "spki",
+ keyData: NjsStringOrBuffer,
+ algorithm: ImportAlgorithm,
+ extractable: boolean,
+ usage: Array<string>): Promise<CryptoKey>;
+
+ /**
+ * Generates a digital signature.
+ *
+ * @param algorithm String or object that specifies the signature
+ * algorithm to use and its parameters.
+ * @param key CryptoKey containing the key to be used for signing.
+ * @param data Data to be signed.
+ */
+ sign(algorithm: SignOrVerifyAlgorithm,
+ key: CryptoKey,
+ data: NjsStringOrBuffer): Promise<ArrayBuffer>;
+
+ /**
+ * Verifies a digital signature.
+ *
+ * @param algorithm String or object that specifies the signature
+ * algorithm to use and its parameters.
+ * @param key CryptoKey containing the key to be used for verifying.
+ * @param signature Signature to verify.
+ * @param data Data to be verified.
+ */
+ verify(algorithm: SignOrVerifyAlgorithm,
+ key: CryptoKey,
+ signature: NjsStringOrBuffer,
+ data: NjsStringOrBuffer): Promise<boolean>;
+}
+
+interface Crypto {
+ readonly subtle: SubtleCrypto;
+ getRandomValues(ta:TypedArray): TypedArray;
+}
+
+declare const crypto: Crypto;