]> git.kaiwu.me - njs.git/commitdiff
WebCrypto: added AES-KW algorithm support.
authorDmitry Volyntsev <xeioex@nginx.com>
Fri, 3 Apr 2026 05:32:24 +0000 (22:32 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 6 Apr 2026 22:24:37 +0000 (15:24 -0700)
Supports 128, 192, and 256-bit key sizes with generateKey,
importKey, and exportKey operations in raw and JWK formats.

Also fixed deriveKey to accept 192-bit AES key lengths.

auto/openssl
external/njs_webcrypto_module.c
external/qjs_webcrypto_module.c
test/webcrypto/aes_kw.t.mjs [new file with mode: 0644]
test/webcrypto/wrap.t.mjs [new file with mode: 0644]
ts/njs_webcrypto.d.ts

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