From: Dmitry Volyntsev Date: Mon, 11 Oct 2021 15:06:15 +0000 (+0000) Subject: Introduced WebCrypto API according to W3C spec. X-Git-Tag: 0.7.0~6 X-Git-Url: http://www.kaiwu.me/postgresql/commit/?a=commitdiff_plain;h=7b2b7612dc4ee6370b93462602a9892f97d155b9;p=njs.git 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 b66864eb..e5ba7bf6 100644 --- a/auto/make +++ b/auto/make @@ -75,7 +75,7 @@ cat << END >> $NJS_MAKEFILE $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 @@ njs_dep_post=`njs_gen_dep_post $njs_dep $njs_externals_obj` 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 00000000..1140c6ff --- /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 + + 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 + #include + + 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 + + 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 4e5e65cc..9544e097 100644 --- a/auto/sources +++ b/auto/sources @@ -2,6 +2,7 @@ NJS_LIB_SRCS=" \ 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 9399ed92..90bac3de 100644 --- a/auto/summary +++ b/auto/summary @@ -15,6 +15,10 @@ if [ $NJS_HAVE_READLINE = YES ]; then 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 f3e845dc..9e848233 100755 --- a/configure +++ b/configure @@ -26,12 +26,13 @@ set -u . 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 00000000..00f9e63f --- /dev/null +++ b/external/njs_webcrypto.c @@ -0,0 +1,2666 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) NGINX, Inc. + */ + + +#include +#include "njs_webcrypto.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if NJS_HAVE_OPENSSL_HKDF +#include +#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 00000000..3331b57b --- /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 29f6dcac..7ebdcfe9 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 @@ if [ $HTTP != NO ]; then 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 @@ if [ $STREAM != NO ]; then 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 ae7955ec..012adb48 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -10,6 +10,7 @@ #include #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 @@ ngx_js_core_init(njs_vm_t *vm, ngx_log_t *log) 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 bfa9a375..5abb1719 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 @@ njs_externals_init(njs_vm_t *vm, njs_console_t *console) 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 00000000..fdad3731 --- /dev/null +++ b/src/njs_str.c @@ -0,0 +1,37 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) NGINX, Inc. + */ + + +#include + + +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 5bc013de..ae4dd082 100644 --- a/src/njs_str.h +++ b/src/njs_str.h @@ -134,4 +134,13 @@ njs_strstr_eq(s1, s2) \ && (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 c52e65b7..3285b16e 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_externals_init_internal(njs_vm_t *vm, njs_unit_test_req_init_t *init, 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 ae97e76e..4309c508 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_disabled_denormals_test[] = }; +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 @@ static njs_unit_test_t njs_externals_test[] = { 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 +#define N262 "" +#endif + +#if (NJS_HAVE_OPENSSL) +#define NCRYPTO "crypto," #else - njs_str("$r,$r2,$r3,$shared,global,njs,process") }, +#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 @@ static njs_test_suite_t njs_suites[] = 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 cc3bebeb..9b2287aa 100644 --- a/test/njs_expect_test.exp +++ b/test/njs_expect_test.exp @@ -1113,6 +1113,38 @@ njs_run {"./test/js/promise_race.js"} \ 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 4ec9db0c..a30c5883 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 @@ function crypto_module(str: NjsByteString) { 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 00000000..a06d1b49 --- /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 00000000..1b8219f5 --- /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 00000000..3b4dfe78 --- /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 00000000..f286d836 --- /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 00000000..e2f6917d --- /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 00000000..4eed191b --- /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 00000000..98297941 --- /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 00000000..c9f60584 --- /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 00000000..b835530c --- /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 00000000..afde0807 --- /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 00000000..36744d84 --- /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 00000000..0065b981 --- /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 00000000..6341afe9 --- /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 00000000..6ff75cff --- /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 00000000..d3f35d8e --- /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 00000000..2520b2d0 --- /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 00000000..fde29383 --- /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 00000000..c5e0f654 --- /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 00000000..0473d2ac --- /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 00000000..8ee6ad68 --- /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 00000000..b4b623f3 --- /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 00000000..4ed5331a --- /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 00000000..f1b4a414 --- /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 00000000..64724bbd --- /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 00000000..4bb99b49 --- /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 00000000..8b497d87 --- /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 00000000..7bcbb121 --- /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 00000000..829f0590 --- /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 00000000..be59cc2e --- /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 00000000..25159637 --- /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 00000000..95ba1e5b --- /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 00000000..0587647c --- /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 00000000..c7e72be9 --- /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 00000000..130cc62f --- /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 00000000..f2b59c0e --- /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 00000000..078d42dd --- /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 00000000..ef844203 --- /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 00000000..184bcf83 --- /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 00000000..a969e3ba --- /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 8a324cc6..eec5c342 100644 --- a/ts/index.d.ts +++ b/ts/index.d.ts @@ -1,4 +1,5 @@ /// +/// /// /// /// diff --git a/ts/njs_core.d.ts b/ts/njs_core.d.ts index 808c08f2..70378452 100644 --- a/ts/njs_core.d.ts +++ b/ts/njs_core.d.ts @@ -584,7 +584,7 @@ declare class Buffer extends Uint8Array { 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 00000000..b67ee0d5 --- /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; + + /** + * 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; + + /** + * 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): Promise; + + /** + * Generates a digest of the given data. + * + * @param algorithm String defining the hash function to use. + */ + digest(algorithm: HashVariants, + data: NjsStringOrBuffer): Promise; + + /** + * 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; + + /** + * 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): Promise; + + /** + * 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; + + /** + * 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; +} + +interface Crypto { + readonly subtle: SubtleCrypto; + getRandomValues(ta:TypedArray): TypedArray; +} + +declare const crypto: Crypto;