View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.crypto;
16  
17  import java.math.BigInteger;
18  import java.nio.ByteBuffer;
19  import java.security.GeneralSecurityException;
20  import java.security.Key;
21  import java.security.KeyPair;
22  import java.security.KeyPairGenerator;
23  import java.security.MessageDigest;
24  import java.security.NoSuchAlgorithmException;
25  import java.security.PrivateKey;
26  import java.security.PublicKey;
27  import java.security.Signature;
28  import java.security.SignatureException;
29  import java.security.spec.AlgorithmParameterSpec;
30  import java.security.spec.ECGenParameterSpec;
31  import java.security.spec.MGF1ParameterSpec;
32  import java.security.spec.PSSParameterSpec;
33  import java.security.spec.RSAKeyGenParameterSpec;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.HashMap;
37  import java.util.LinkedHashSet;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Set;
41  
42  import javax.crypto.BadPaddingException;
43  import javax.crypto.Cipher;
44  import javax.crypto.KeyGenerator;
45  import javax.crypto.Mac;
46  import javax.crypto.NoSuchPaddingException;
47  import javax.crypto.SecretKey;
48  import javax.crypto.spec.GCMParameterSpec;
49  import javax.crypto.spec.IvParameterSpec;
50  import javax.crypto.spec.OAEPParameterSpec;
51  import javax.crypto.spec.PSource;
52  import javax.crypto.spec.SecretKeySpec;
53  
54  import org.htmlunit.corejs.javascript.Context;
55  import org.htmlunit.corejs.javascript.EcmaError;
56  import org.htmlunit.corejs.javascript.NativePromise;
57  import org.htmlunit.corejs.javascript.Scriptable;
58  import org.htmlunit.corejs.javascript.ScriptableObject;
59  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
60  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
61  import org.htmlunit.javascript.HtmlUnitScriptable;
62  import org.htmlunit.javascript.JavaScriptEngine;
63  import org.htmlunit.javascript.configuration.JsxClass;
64  import org.htmlunit.javascript.configuration.JsxConstructor;
65  import org.htmlunit.javascript.configuration.JsxFunction;
66  import org.htmlunit.javascript.host.dom.DOMException;
67  
68  /**
69   * A JavaScript object for {@code SubtleCrypto}.
70   *
71   * @author Ahmed Ashour
72   * @author Ronald Brill
73   * @author Atsushi Nakagawa
74   * @author Lai Quang Duong
75   *
76   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto">MDN Documentation</a>
77   */
78  @JsxClass
79  public class SubtleCrypto extends HtmlUnitScriptable {
80  
81      /**
82       * Maps each crypto operation to its supported algorithm names.
83       * @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
84       */
85      private static final Map<String, Set<String>> OPERATION_TO_SUPPORTED_ALGORITHMS = Map.ofEntries(
86              Map.entry("encrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
87              Map.entry("decrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
88              Map.entry("sign", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
89              Map.entry("verify", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
90              Map.entry("digest", Set.of("SHA-1", "SHA-256", "SHA-384", "SHA-512")),
91              Map.entry("generateKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP",
92                      "ECDSA", "ECDH", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC")),
93              Map.entry("importKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP", "ECDSA", "ECDH",
94                      "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC", "HKDF", "PBKDF2")),
95              Map.entry("wrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
96              Map.entry("unwrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
97              Map.entry("deriveBits", Set.of("ECDH", "HKDF", "PBKDF2")),
98              Map.entry("deriveKey", Set.of("ECDH", "HKDF", "PBKDF2"))
99      );
100 
101     /**
102      * The registered algorithm names, keyed by their ASCII lowercase form.
103      * @see <a href="https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm">
104      *     Normalize an algorithm</a>
105      */
106     private static final Map<String, String> REGISTERED_ALGORITHM_NAMES = new HashMap<>();
107 
108     static {
109         for (final Set<String> names : OPERATION_TO_SUPPORTED_ALGORITHMS.values()) {
110             for (final String name : names) {
111                 REGISTERED_ALGORITHM_NAMES.put(toAsciiLowerCase(name), name);
112             }
113         }
114     }
115 
116     /**
117      * The recognized cryptographic key usages.
118      *
119      * @see <a href="https://w3c.github.io/webcrypto/#dfn-RecognizedKeyUsage">RecognizedKeyUsage</a>
120      */
121     private static final Set<String> RECOGNIZED_KEY_USAGES = Collections.unmodifiableSet(
122             new LinkedHashSet<>(List.of("encrypt", "decrypt", "sign", "verify",
123                     "deriveKey", "deriveBits", "wrapKey", "unwrapKey")));
124 
125     /**
126      * The key formats.
127      * @see <a href="https://w3c.github.io/webcrypto/#dfn-KeyFormat">KeyFormat</a>
128      */
129     private static final Set<String> KEY_FORMATS = Set.of("raw", "spki", "pkcs8", "jwk");
130 
131     /**
132      * The valid authentication tag lengths for AES-GCM, in bits.
133      *
134      * @see <a href="https://w3c.github.io/webcrypto/#aes-gcm-operations">AES-GCM encrypt, step 6</a>
135      */
136     private static final Set<Integer> VALID_AES_GCM_TAG_LENGTHS = Set.of(32, 64, 96, 104, 112, 120, 128);
137 
138     private static class InvalidAccessException extends RuntimeException {
139         InvalidAccessException(final String message) {
140             super(message);
141         }
142     }
143 
144     private static class DataException extends RuntimeException {
145         DataException(final String message) {
146             super(message);
147         }
148     }
149 
150     private static class OperationException extends RuntimeException {
151         OperationException(final String message) {
152             super(message);
153         }
154     }
155 
156     /**
157      * Creates an instance.
158      */
159     @JsxConstructor
160     public void jsConstructor() {
161         throw JavaScriptEngine.typeErrorIllegalConstructor();
162     }
163 
164     private NativePromise rejectNotACryptoKey(final String operation) {
165         return rejectWithTypeError("Failed to execute '" + operation
166                 + "' on 'SubtleCrypto': parameter 2 is not of type 'CryptoKey'.");
167     }
168 
169     private NativePromise rejectWithTypeError(final String message) {
170         return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(
171                 JavaScriptEngine.typeError(message), getParentScope()));
172     }
173 
174     private NativePromise notImplemented() {
175         return setupRejectedPromise(() ->
176                 createDOMException("Operation is not supported", DOMException.NOT_SUPPORTED_ERR));
177     }
178 
179     /**
180      * Encrypts data using the given key and algorithm.
181      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-encrypt">SubtleCrypto.encrypt()</a>
182      * @param algorithm the algorithm identifier with parameters
183      * @param key the CryptoKey to encrypt with
184      * @param data the data to encrypt
185      * @return a Promise that fulfills with an ArrayBuffer containing the ciphertext
186      */
187     @JsxFunction
188     public NativePromise encrypt(final Object algorithm, final Object key, final Object data) {
189         if (!(key instanceof CryptoKey cryptoKey)) {
190             return rejectNotACryptoKey("encrypt");
191         }
192 
193         return doCipher(algorithm, cryptoKey, data, Cipher.ENCRYPT_MODE);
194     }
195 
196     /**
197      * Decrypts data using the given key and algorithm.
198      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-decrypt">SubtleCrypto.decrypt()</a>
199      * @param algorithm the algorithm identifier with parameters
200      * @param key the CryptoKey to decrypt with
201      * @param data the data to decrypt
202      * @return a Promise that fulfills with an ArrayBuffer containing the plaintext
203      */
204     @JsxFunction
205     public NativePromise decrypt(final Object algorithm, final Object key, final Object data) {
206         if (!(key instanceof CryptoKey cryptoKey)) {
207             return rejectNotACryptoKey("decrypt");
208         }
209 
210         return doCipher(algorithm, cryptoKey, data, Cipher.DECRYPT_MODE);
211     }
212 
213     /**
214      * Shared encrypt/decrypt implementation.
215      */
216     private NativePromise doCipher(final Object algorithm, final CryptoKey key,
217             final Object data, final int cipherMode) {
218         final String operation = switch (cipherMode) {
219             case Cipher.ENCRYPT_MODE -> "encrypt";
220             case Cipher.DECRYPT_MODE -> "decrypt";
221             default -> throw new IllegalArgumentException("Invalid cipher mode: " + cipherMode);
222         };
223 
224         final byte[] result;
225         try {
226             final String algorithmName = resolveAlgorithmName(algorithm);
227             ensureAlgorithmIsSupported(operation, algorithmName);
228             ensureKeyAlgorithmMatches(algorithmName, key);
229             ensureKeyUsage(key, operation);
230 
231             final ByteBuffer inputData = asByteBuffer(data);
232 
233             switch (algorithmName) {
234                 case "AES-CBC": {
235                     // https://w3c.github.io/webcrypto/#aes-cbc-operations
236                     final byte[] iv = getRequiredBuffer(toAlgorithmParams(algorithm, algorithmName), "iv");
237                     if (iv.length != 16) {
238                         throw new OperationException("The iv must be 16 bytes long");
239                     }
240                     final byte[] input = toByteArray(inputData);
241                     if (cipherMode == Cipher.DECRYPT_MODE && (input.length == 0 || input.length % 16 != 0)) {
242                         throw new OperationException("The ciphertext length must be a non-zero multiple of 16 bytes");
243                     }
244                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
245                     final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
246                     cipher.init(cipherMode, secretKey, new IvParameterSpec(iv));
247                     result = cipher.doFinal(input);
248                     break;
249                 }
250                 case "AES-GCM": {
251                     // https://w3c.github.io/webcrypto/#aes-gcm-operations
252                     final Scriptable params = toAlgorithmParams(algorithm, algorithmName);
253                     // the spec only defines an upper limit; browsers reject an empty iv with OperationError
254                     final byte[] iv = getRequiredBuffer(params, "iv");
255                     if (iv.length == 0) {
256                         throw new OperationException("The iv must not be empty");
257                     }
258 
259                     final int tagLength;
260                     final Object tagLengthProp = ScriptableObject.getProperty(params, "tagLength");
261                     if (tagLengthProp instanceof Number num) {
262                         tagLength = num.intValue();
263                         if (!VALID_AES_GCM_TAG_LENGTHS.contains(tagLength)) {
264                             throw new OperationException("The tag length " + tagLength + " is not supported");
265                         }
266                     }
267                     else {
268                         tagLength = 128;
269                     }
270 
271                     final byte[] input = toByteArray(inputData);
272                     if (cipherMode == Cipher.DECRYPT_MODE && input.length * 8L < tagLength) {
273                         throw new OperationException("The ciphertext is shorter than the tag");
274                     }
275 
276                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
277                     final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
278                     cipher.init(cipherMode, secretKey, new GCMParameterSpec(tagLength, iv));
279 
280                     final Object aadProp = ScriptableObject.getProperty(params, "additionalData");
281                     if (aadProp instanceof Scriptable) {
282                         final ByteBuffer aad = asByteBuffer(aadProp);
283                         cipher.updateAAD(toByteArray(aad));
284                     }
285 
286                     result = cipher.doFinal(input);
287                     break;
288                 }
289                 case "AES-CTR": {
290                     // https://w3c.github.io/webcrypto/#aes-ctr-operations
291                     final Scriptable params = toAlgorithmParams(algorithm, algorithmName);
292                     final byte[] counter = getRequiredBuffer(params, "counter");
293                     if (counter.length != 16) {
294                         throw new OperationException("The counter must be 16 bytes long");
295                     }
296 
297                     final int counterLength = (int) getRequiredNumber(params, "length");
298                     if (counterLength < 1 || counterLength > 128) {
299                         throw new OperationException("The counter length must be between 1 and 128 bits");
300                     }
301 
302                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
303                     result = aesCtr(cipherMode, secretKey, counter, counterLength, toByteArray(inputData));
304                     break;
305                 }
306                 case "RSA-OAEP": {
307                     // https://w3c.github.io/webcrypto/#rsa-oaep-operations
308                     final String hash = ((RsaHashedKeyAlgorithm) key.getKeyAlgorithm()).getHash();
309 
310                     // all RsaOaepParams members are optional, the algorithm may be a plain name
311                     final byte[] label;
312                     final Object labelProp = algorithm instanceof Scriptable params
313                             ? ScriptableObject.getProperty(params, "label")
314                             : Scriptable.NOT_FOUND;
315                     if (labelProp instanceof Scriptable) {
316                         final ByteBuffer labelBuf = asByteBuffer(labelProp);
317                         label = toByteArray(labelBuf);
318                     }
319                     else {
320                         label = new byte[0];
321                     }
322 
323                     final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hash);
324                     final AlgorithmParameterSpec oaepSpec = new OAEPParameterSpec(
325                             hash, "MGF1", mgf1Spec, new PSource.PSpecified(label));
326 
327                     final Key internalKey;
328                     if (cipherMode == Cipher.ENCRYPT_MODE) {
329                         internalKey = getInternalKey(key, PublicKey.class);
330                     }
331                     else {
332                         internalKey = getInternalKey(key, PrivateKey.class);
333                     }
334 
335                     final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
336                     cipher.init(cipherMode, internalKey, oaepSpec);
337                     result = cipher.doFinal(toByteArray(inputData));
338                     break;
339                 }
340                 default:
341                     throw new UnsupportedOperationException(operation + " " + algorithmName);
342             }
343         }
344         catch (final EcmaError e) {
345             return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(e, getParentScope()));
346         }
347         catch (final InvalidAccessException e) {
348             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.INVALID_ACCESS_ERR));
349         }
350         catch (final DataException e) {
351             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.DATA_ERR));
352         }
353         catch (final OperationException e) {
354             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.OPERATION_ERR));
355         }
356         catch (final IllegalArgumentException e) {
357             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
358         }
359         catch (final BadPaddingException e) {
360             return setupRejectedPromise(() -> createDOMException("Decryption failed", DOMException.OPERATION_ERR));
361         }
362         catch (final NoSuchAlgorithmException | NoSuchPaddingException | UnsupportedOperationException e) {
363             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
364                     DOMException.NOT_SUPPORTED_ERR));
365         }
366         catch (final GeneralSecurityException e) {
367             return setupRejectedPromise(() -> createDOMException("The operation failed: " + e.getMessage(),
368                     DOMException.OPERATION_ERR));
369         }
370         return setupPromise(() -> createArrayBuffer(result));
371     }
372 
373     /**
374      * AES-CTR encryption/decryption where only the rightmost {@code counterLength} bits of the
375      * counter block are incremented; Java's AES/CTR always increments the whole 128-bit block.
376      * @see <a href="https://w3c.github.io/webcrypto/#aes-ctr-operations">AES-CTR</a>
377      */
378     private static byte[] aesCtr(final int cipherMode, final SecretKey key, final byte[] counter,
379             final int counterLength, final byte[] input) throws GeneralSecurityException {
380         final BigInteger blocks = BigInteger.valueOf((input.length + 15L) / 16);
381         final BigInteger counterValues = BigInteger.ONE.shiftLeft(counterLength);
382         if (blocks.compareTo(counterValues) > 0) {
383             throw new DataException("The counter would repeat");
384         }
385 
386         final BigInteger counterBlock = new BigInteger(1, counter);
387         final BigInteger counterPart = counterBlock.mod(counterValues);
388         final BigInteger blocksUntilWrap = counterValues.subtract(counterPart);
389 
390         final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
391         cipher.init(cipherMode, key, new IvParameterSpec(counter));
392         if (blocks.compareTo(blocksUntilWrap) <= 0) {
393             return cipher.doFinal(input);
394         }
395 
396         // process up to the wrap with the given counter block, the rest starts with the counter part set to zero
397         final int firstLength = blocksUntilWrap.intValueExact() * 16;
398         final byte[] result = new byte[input.length];
399         cipher.doFinal(input, 0, firstLength, result, 0);
400 
401         final Cipher wrapped = Cipher.getInstance("AES/CTR/NoPadding");
402         wrapped.init(cipherMode, key, new IvParameterSpec(toCounterBlock(counterBlock.subtract(counterPart))));
403         wrapped.doFinal(input, firstLength, input.length - firstLength, result, firstLength);
404         return result;
405     }
406 
407     private static byte[] toCounterBlock(final BigInteger value) {
408         final byte[] bytes = value.toByteArray();
409         final byte[] block = new byte[16];
410         final int length = Math.min(bytes.length, 16);
411         System.arraycopy(bytes, bytes.length - length, block, 16 - length, length);
412         return block;
413     }
414 
415     /**
416      * Signs data using the given key.
417      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-sign">SubtleCrypto.sign()</a>
418      * @param algorithm the algorithm identifier (String or object with name property)
419      * @param key the CryptoKey to sign with
420      * @param data the data to sign
421      * @return a Promise that fulfills with an ArrayBuffer containing the signature
422      */
423     @JsxFunction
424     public NativePromise sign(final Object algorithm, final Object key, final Object data) {
425         if (!(key instanceof CryptoKey cryptoKey)) {
426             return rejectNotACryptoKey("sign");
427         }
428 
429         return doSignOrVerify(algorithm, cryptoKey, null, data, true);
430     }
431 
432     /**
433      * Verifies a signature using the given key.
434      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify">SubtleCrypto.verify()</a>
435      * @param algorithm the algorithm identifier (String or object with name property)
436      * @param key the CryptoKey to verify with
437      * @param signature the signature to verify
438      * @param data the data that was signed
439      * @return a Promise that fulfills with a boolean indicating whether the signature is valid
440      */
441     @JsxFunction
442     public NativePromise verify(final Object algorithm, final Object key,
443             final Object signature, final Object data) {
444         if (!(key instanceof CryptoKey cryptoKey)) {
445             return rejectNotACryptoKey("verify");
446         }
447 
448         return doSignOrVerify(algorithm, cryptoKey, signature, data, false);
449     }
450 
451     /**
452      * Shared sign/verify implementation.
453      */
454     private NativePromise doSignOrVerify(final Object algorithm, final CryptoKey key,
455             final Object existingSignature, final Object data, final boolean isSigning) {
456         final Object result;
457         try {
458             final String algorithmName = resolveAlgorithmName(algorithm);
459             final String operation = isSigning ? "sign" : "verify";
460             ensureAlgorithmIsSupported(operation, algorithmName);
461             ensureKeyAlgorithmMatches(algorithmName, key);
462             ensureKeyUsage(key, operation);
463 
464             final ByteBuffer inputData = asByteBuffer(data);
465 
466             switch (algorithmName) {
467                 case "HMAC": {
468                     // https://w3c.github.io/webcrypto/#hmac-operations
469                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
470                     final Mac mac = Mac.getInstance(secretKey.getAlgorithm());
471                     mac.init(secretKey);
472                     mac.update(inputData);
473                     final byte[] macBytes = mac.doFinal();
474                     if (isSigning) {
475                         result = macBytes;
476                     }
477                     else {
478                         result = MessageDigest.isEqual(macBytes,
479                                 toByteArray(asByteBuffer(existingSignature)));
480                     }
481                     break;
482                 }
483                 case "RSASSA-PKCS1-v1_5":
484                     // https://w3c.github.io/webcrypto/#rsassa-pkcs1
485                 case "RSA-PSS":
486                     // https://w3c.github.io/webcrypto/#rsa-pss
487                 case "ECDSA": {
488                     // https://w3c.github.io/webcrypto/#ecdsa-operations
489                     final Signature sig = "ECDSA".equals(algorithmName)
490                             ? resolveEcdsaSignature(algorithm)
491                             : resolveRsaSignature(algorithmName, algorithm, key);
492                     if (isSigning) {
493                         sig.initSign(getInternalKey(key, PrivateKey.class));
494                         sig.update(inputData);
495                         result = sig.sign();
496                     }
497                     else {
498                         final byte[] signature = toByteArray(asByteBuffer(existingSignature));
499                         sig.initVerify(getInternalKey(key, PublicKey.class));
500                         sig.update(inputData);
501 
502                         boolean valid;
503                         try {
504                             valid = sig.verify(signature);
505                         }
506                         catch (final SignatureException e) {
507                             // e.g. an RSA signature that does not have the length of the modulus
508                             valid = false;
509                         }
510                         result = valid;
511                     }
512                     break;
513                 }
514                 default:
515                     throw new UnsupportedOperationException(operation + " " + algorithmName);
516             }
517         }
518         catch (final EcmaError e) {
519             return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(e, getParentScope()));
520         }
521         catch (final InvalidAccessException e) {
522             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.INVALID_ACCESS_ERR));
523         }
524         catch (final IllegalArgumentException e) {
525             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
526         }
527         catch (final NoSuchAlgorithmException | UnsupportedOperationException e) {
528             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
529                     DOMException.NOT_SUPPORTED_ERR));
530         }
531         catch (final GeneralSecurityException e) {
532             return setupRejectedPromise(() -> createDOMException("The operation failed: " + e.getMessage(),
533                     DOMException.OPERATION_ERR));
534         }
535 
536         if (isSigning) {
537             return setupPromise(() -> createArrayBuffer((byte[]) result));
538         }
539         return setupPromise(() -> result);
540     }
541 
542     /**
543      * Resolves the RSA {@link Signature} instance for the given algorithm.
544      */
545     private static Signature resolveRsaSignature(final String algorithmName, final Object algorithmParams,
546             final CryptoKey key) throws GeneralSecurityException {
547         final String hash = ((RsaHashedKeyAlgorithm) key.getKeyAlgorithm()).getHash();
548         final String javaHash = hash.replace("-", "");
549 
550         if ("RSASSA-PKCS1-v1_5".equals(algorithmName)) {
551             return Signature.getInstance(javaHash + "withRSA");
552         }
553 
554         final int saltLength = (int) getRequiredNumber(toAlgorithmParams(algorithmParams, algorithmName), "saltLength");
555 
556         final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hash);
557         final PSSParameterSpec pssSpec = new PSSParameterSpec(hash, "MGF1", mgf1Spec, saltLength, 1);
558         final Signature sig = Signature.getInstance("RSASSA-PSS");
559         sig.setParameter(pssSpec);
560         return sig;
561     }
562 
563     /**
564      * Resolves the ECDSA {@link Signature} instance for the given algorithm params.
565      */
566     private static Signature resolveEcdsaSignature(final Object algorithmParams)
567             throws GeneralSecurityException {
568         final Scriptable params = toAlgorithmParams(algorithmParams, "ECDSA");
569         final String hash = resolveAlgorithmName(getRequiredMember(params, "hash"));
570         final String javaHash = hash.replace("-", "");
571         return Signature.getInstance(javaHash + "withECDSAinP1363Format");
572     }
573 
574     private static byte[] toByteArray(final ByteBuffer buffer) {
575         final byte[] result = new byte[buffer.remaining()];
576         buffer.get(result);
577         return result;
578     }
579 
580     /**
581      * Generates a digest of the given data.
582      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest">SubtleCrypto.digest()</a>
583      * @param hashAlgorithm a string or an object with a single property name containing the hash algorithm to use
584      * @param data an object containing the data to be digested
585      * @return a Promise that fulfills with an ArrayBuffer containing the digest
586      */
587     @JsxFunction
588     public NativePromise digest(final Object hashAlgorithm, final Object data) {
589         final byte[] digest;
590         try {
591             final ByteBuffer inputData = asByteBuffer(data);
592             final String algorithm = resolveAlgorithmName(hashAlgorithm);
593             ensureAlgorithmIsSupported("digest", algorithm);
594 
595             final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
596             messageDigest.update(inputData);
597             digest = messageDigest.digest();
598         }
599         catch (final EcmaError e) {
600             return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(e, getParentScope()));
601         }
602         catch (final IllegalArgumentException e) {
603             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
604         }
605         catch (final GeneralSecurityException | UnsupportedOperationException e) {
606             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
607                     DOMException.NOT_SUPPORTED_ERR));
608         }
609         return setupPromise(() -> createArrayBuffer(digest));
610     }
611 
612     /**
613      * Generates a new key (for symmetric algorithms) or key pair (for public-key algorithms).
614      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-generateKey">SubtleCrypto.generateKey()</a>
615      * @param keyGenParams algorithm-specific key generation parameters
616      * @param isExtractable whether the key(s) can be exported
617      * @param keyUsages permitted operations for the key(s)
618      * @return a Promise that fulfills with a CryptoKey or CryptoKeyPair
619      */
620     @JsxFunction
621     public NativePromise generateKey(final Object keyGenParams, final boolean isExtractable,
622             final Object keyUsages) {
623         final Object result;
624         try {
625             final List<String> requestedUsages = toKeyUsages(keyUsages);
626             final String algorithm = resolveAlgorithmName(keyGenParams);
627             ensureAlgorithmIsSupported("generateKey", algorithm);
628 
629             final Scriptable params = toAlgorithmParams(keyGenParams, algorithm);
630 
631             switch (algorithm) {
632                 case "RSASSA-PKCS1-v1_5":
633                 case "RSA-PSS":
634                 case "RSA-OAEP": {
635                     final RsaHashedKeyAlgorithm rsaParams = RsaHashedKeyAlgorithm.from(params);
636                     final List<String> usages = resolveKeyUsages(algorithm, requestedUsages);
637 
638                     final KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
639                     keyPairGen.initialize(new RSAKeyGenParameterSpec(
640                             rsaParams.getModulusLength(), rsaParams.getPublicExponentAsBigInteger()));
641                     final KeyPair keyPair = keyPairGen.generateKeyPair();
642 
643                     result = createKeyPair(keyPair, rsaParams, isExtractable, usages);
644                     break;
645                 }
646                 case "ECDSA":
647                 case "ECDH": {
648                     final EcKeyAlgorithm ecParams = EcKeyAlgorithm.from(params);
649                     final List<String> usages = resolveKeyUsages(algorithm, requestedUsages);
650 
651                     final KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
652                     keyPairGen.initialize(new ECGenParameterSpec(ecParams.getJavaCurveName()));
653                     final KeyPair keyPair = keyPairGen.generateKeyPair();
654 
655                     result = createKeyPair(keyPair, ecParams, isExtractable, usages);
656                     break;
657                 }
658                 case "AES-CBC":
659                 case "AES-CTR":
660                 case "AES-GCM":
661                 case "AES-KW": {
662                     final AesKeyAlgorithm aesParams = AesKeyAlgorithm.from(params);
663                     final List<String> usages = resolveKeyUsages(algorithm, requestedUsages);
664                     if (usages.isEmpty()) {
665                         throw new IllegalArgumentException("An invalid or illegal string was specified");
666                     }
667                     if (!AesKeyAlgorithm.SUPPORTED_LENGTHS.contains(aesParams.getLength())) {
668                         throw new OperationException("The AES key length must be 128, 192 or 256 bits");
669                     }
670 
671                     final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
672                     keyGen.init(aesParams.getLength());
673                     final SecretKey secretKey = keyGen.generateKey();
674 
675                     result = CryptoKey.create(getParentScope(), secretKey, isExtractable, aesParams, usages);
676                     break;
677                 }
678                 case "HMAC": {
679                     final HmacKeyAlgorithm hmacParams = HmacKeyAlgorithm.from(params);
680                     final List<String> usages = resolveKeyUsages(algorithm, requestedUsages);
681                     if (usages.isEmpty()) {
682                         throw new IllegalArgumentException("An invalid or illegal string was specified");
683                     }
684                     if (hmacParams.getLength() == 0) {
685                         throw new OperationException("The HMAC key length must not be 0");
686                     }
687 
688                     final KeyGenerator keyGen = KeyGenerator.getInstance(hmacParams.getJavaName());
689                     keyGen.init(hmacParams.getLength());
690                     final SecretKey secretKey = keyGen.generateKey();
691 
692                     result = CryptoKey.create(getParentScope(), secretKey, isExtractable, hmacParams, usages);
693                     break;
694                 }
695                 default:
696                     throw new UnsupportedOperationException("generateKey " + algorithm);
697             }
698         }
699         catch (final EcmaError e) {
700             return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(e, getParentScope()));
701         }
702         catch (final OperationException e) {
703             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.OPERATION_ERR));
704         }
705         catch (final IllegalArgumentException e) {
706             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
707         }
708         catch (final NoSuchAlgorithmException | UnsupportedOperationException e) {
709             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
710                     DOMException.NOT_SUPPORTED_ERR));
711         }
712         catch (final GeneralSecurityException e) {
713             return setupRejectedPromise(() -> createDOMException("The operation failed: " + e.getMessage(),
714                     DOMException.OPERATION_ERR));
715         }
716         return setupPromise(() -> result);
717     }
718 
719     /**
720      * Creates a CryptoKeyPair (plain JS object with publicKey/privateKey) from a Java KeyPair.
721      * The public key is always extractable regardless of the extractable parameter.
722      * Usages are split: public gets {encrypt,verify,wrapKey},
723      * private gets {decrypt,sign,unwrapKey,deriveBits,deriveKey}.
724      */
725     private Scriptable createKeyPair(final KeyPair keyPair, final KeyAlgorithm algorithm,
726             final boolean isExtractable, final List<String> allUsages) {
727         final Set<String> publicUsageSet = Set.of("encrypt", "verify", "wrapKey");
728         final Set<String> privateUsageSet = Set.of("decrypt", "sign", "unwrapKey", "deriveBits", "deriveKey");
729 
730         final List<String> publicUsages = new ArrayList<>();
731         final List<String> privateUsages = new ArrayList<>();
732         for (final String usage : allUsages) {
733             if (publicUsageSet.contains(usage)) {
734                 publicUsages.add(usage);
735             }
736             if (privateUsageSet.contains(usage)) {
737                 privateUsages.add(usage);
738             }
739         }
740 
741         // if privateKey usages would be empty, throw SyntaxError
742         if (privateUsages.isEmpty()) {
743             throw new IllegalArgumentException("An invalid or illegal string was specified");
744         }
745 
746         // public key is always extractable
747         final CryptoKey publicKey = CryptoKey.create(
748                 getParentScope(), keyPair.getPublic(), true, algorithm, publicUsages);
749         final CryptoKey privateKey = CryptoKey.create(
750                 getParentScope(), keyPair.getPrivate(), isExtractable, algorithm, privateUsages);
751 
752         final Scriptable keyPairObj = JavaScriptEngine.newObject(getParentScope());
753         ScriptableObject.putProperty(keyPairObj, "publicKey", publicKey);
754         ScriptableObject.putProperty(keyPairObj, "privateKey", privateKey);
755         return keyPairObj;
756     }
757 
758     /**
759      * Not yet implemented.
760      *
761      * @return a Promise which will be fulfilled with a CryptoKey object representing the new key
762      */
763     @JsxFunction
764     public NativePromise deriveKey() {
765         return notImplemented();
766     }
767 
768     /**
769      * Not yet implemented.
770      *
771      * @return a Promise which will be fulfilled with an ArrayBuffer containing the derived bits
772      */
773     @JsxFunction
774     public NativePromise deriveBits() {
775         return notImplemented();
776     }
777 
778     /**
779      * Imports a key from external, portable key material.
780      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-importKey">SubtleCrypto.importKey()</a>
781      * @param format the data format ("raw", "pkcs8", "spki", "jwk")
782      * @param keyData the key material (BufferSource for raw/pkcs8/spki, JsonWebKey for jwk)
783      * @param keyImportParams algorithm-specific import parameters
784      * @param isExtractable whether the key can be exported
785      * @param keyUsages permitted operations for this key
786      * @return a Promise that fulfills with the imported CryptoKey
787      */
788     @JsxFunction
789     public NativePromise importKey(final String format, final Scriptable keyData,
790             final Object keyImportParams, final boolean isExtractable, final Object keyUsages) {
791 
792         final CryptoKey key;
793         try {
794             ensureKeyFormat(format);
795             final List<String> requestedUsages = toKeyUsages(keyUsages);
796             final String algorithm = resolveAlgorithmName(keyImportParams);
797             ensureAlgorithmIsSupported("importKey", algorithm);
798 
799             if (!"raw".equals(format)) {
800                 return notImplemented();
801             }
802             key = importRawKey(algorithm, keyData, keyImportParams, isExtractable, requestedUsages);
803         }
804         catch (final EcmaError e) {
805             return setupRejectedPromise(() -> JavaScriptEngine.toErrorObject(e, getParentScope()));
806         }
807         catch (final DataException e) {
808             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.DATA_ERR));
809         }
810         catch (final IllegalArgumentException e) {
811             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
812         }
813         catch (final UnsupportedOperationException e) {
814             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
815                     DOMException.NOT_SUPPORTED_ERR));
816         }
817         return setupPromise(() -> key);
818     }
819 
820     private CryptoKey importRawKey(final String algorithm, final Scriptable keyData,
821             final Object keyImportParams, final boolean isExtractable, final List<String> requestedUsages) {
822         // a plain string is only valid for algorithms without import parameters
823         if ("HMAC".equals(algorithm) && !(keyImportParams instanceof Scriptable)) {
824             throw JavaScriptEngine.typeError("HmacImportParams: hash: Missing or not an AlgorithmIdentifier");
825         }
826 
827         final ByteBuffer byteBuffer = asByteBuffer(keyData);
828         final byte[] rawBytes = new byte[byteBuffer.remaining()];
829         byteBuffer.get(rawBytes);
830         final int bitLength = rawBytes.length * 8;
831         if (bitLength == 0) {
832             throw new DataException("The key data must not be empty");
833         }
834 
835         final List<String> usages = resolveKeyUsages(algorithm, requestedUsages);
836 
837         final KeyAlgorithm keyAlgorithm;
838         final SecretKey internalKey;
839         if ("HMAC".equals(algorithm)) {
840             final HmacKeyAlgorithm params = HmacKeyAlgorithm.from((Scriptable) keyImportParams, bitLength);
841             final int length = params.getLength();
842             if (length > bitLength || length <= bitLength - 8) {
843                 throw new DataException("The length " + length + " does not match the key data");
844             }
845 
846             keyAlgorithm = params;
847             internalKey = new SecretKeySpec(rawBytes, params.getJavaName());
848         }
849         else if (AesKeyAlgorithm.isSupported(algorithm)) {
850             if (!AesKeyAlgorithm.SUPPORTED_LENGTHS.contains(bitLength)) {
851                 throw new DataException("The AES key data must be 128, 192 or 256 bits long");
852             }
853             keyAlgorithm = new AesKeyAlgorithm(algorithm, bitLength);
854             internalKey = new SecretKeySpec(rawBytes, "AES");
855         }
856         else {
857             throw new UnsupportedOperationException("importKey raw " + algorithm);
858         }
859 
860         // secret keys need a usage; this is checked after the key data
861         if (usages.isEmpty()) {
862             throw new IllegalArgumentException("An invalid or illegal string was specified");
863         }
864         return CryptoKey.create(getParentScope(), internalKey, isExtractable, keyAlgorithm, usages);
865     }
866 
867     /**
868      * Exports a key in the specified format.
869      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-exportKey">SubtleCrypto.exportKey()</a>
870      * @param format the data format ("raw", "pkcs8", "spki", "jwk")
871      * @param key the CryptoKey to export
872      * @return a Promise that fulfills with the key data
873      */
874     @JsxFunction
875     public NativePromise exportKey(final String format, final Object key) {
876         if (!KEY_FORMATS.contains(format)) {
877             return rejectWithTypeError("'" + format + "' is not a valid key format");
878         }
879         if (!(key instanceof CryptoKey cryptoKey)) {
880             return rejectNotACryptoKey("exportKey");
881         }
882 
883         final byte[] result;
884         try {
885             if (!cryptoKey.getExtractable()) {
886                 return setupRejectedPromise(() -> createDOMException(
887                         "A parameter or an operation is not supported by the underlying object",
888                         DOMException.INVALID_ACCESS_ERR));
889             }
890 
891             if (!"raw".equals(format)) {
892                 return notImplemented();
893             }
894             if (!(cryptoKey.getInternalKey() instanceof SecretKey secretKey)) {
895                 throw new IllegalArgumentException(
896                         "Data provided to an operation does not meet requirements");
897             }
898             result = secretKey.getEncoded();
899         }
900         catch (final IllegalArgumentException e) {
901             return setupRejectedPromise(() -> createDOMException(e.getMessage(), DOMException.SYNTAX_ERR));
902         }
903         catch (final UnsupportedOperationException e) {
904             return setupRejectedPromise(() -> createDOMException("Operation is not supported: " + e.getMessage(),
905                     DOMException.NOT_SUPPORTED_ERR));
906         }
907         return setupPromise(() -> createArrayBuffer(result));
908     }
909 
910     /**
911      * Not yet implemented.
912      *
913      * @return a Promise that fulfills with an ArrayBuffer containing the encrypted exported key
914      */
915     @JsxFunction
916     public NativePromise wrapKey() {
917         return notImplemented();
918     }
919 
920     /**
921      * Not yet implemented.
922      *
923      * @return a Promise that fulfills with the unwrapped key as a CryptoKey object
924      */
925     @JsxFunction
926     public NativePromise unwrapKey() {
927         return notImplemented();
928     }
929 
930     /**
931      * Checks if the specified crypto operation supports the given algorithm.
932      * @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
933      * @param operation the crypto operation (e.g. "digest", "sign")
934      * @param algorithm the algorithm name (e.g. "SHA-256", "HMAC")
935      * @throws UnsupportedOperationException if the operation does not support the algorithm
936      */
937     private static void ensureKeyFormat(final String format) {
938         if (!KEY_FORMATS.contains(format)) {
939             throw JavaScriptEngine.typeError("'" + format + "' is not a valid key format");
940         }
941     }
942 
943     private static void ensureAlgorithmIsSupported(final String operation, final String algorithm) {
944         final Set<String> supportedAlgorithms = OPERATION_TO_SUPPORTED_ALGORITHMS.get(operation);
945         if (supportedAlgorithms == null || !supportedAlgorithms.contains(algorithm)) {
946             throw new UnsupportedOperationException(operation + " " + algorithm);
947         }
948     }
949 
950     /**
951      * Verifies that the operation's algorithm name matches the key's algorithm name.
952      * @param algorithmName the algorithm name from the operation parameters
953      * @param key the CryptoKey being used
954      * @throws InvalidAccessException if the algorithm names don't match
955      */
956     private static void ensureKeyAlgorithmMatches(final String algorithmName, final CryptoKey key) {
957         final String keyAlgoName = key.getKeyAlgorithm().getName();
958         if (!algorithmName.equals(keyAlgoName)) {
959             throw new InvalidAccessException(
960                     "A parameter or an operation is not supported by the underlying object");
961         }
962     }
963 
964     /**
965      * Verifies that the key's usages include the specified usage.
966      * @param key the CryptoKey being used
967      * @param usage the required usage (e.g. "encrypt", "sign")
968      * @throws InvalidAccessException if the key doesn't have the required usage
969      */
970     private static void ensureKeyUsage(final CryptoKey key, final String usage) {
971         if (!key.getUsagesInternal().contains(usage)) {
972             throw new InvalidAccessException(
973                     "A parameter or an operation is not supported by the underlying object");
974         }
975     }
976 
977     /**
978      * Resolves the algorithm name from the given {@code AlgorithmIdentifier}.
979      * Registered names are matched ASCII case-insensitive and returned in their registered
980      * spelling (e.g. "aes-gcm" is resolved to "AES-GCM").
981      * @see <a href="https://w3c.github.io/webcrypto/#dfn-AlgorithmIdentifier">
982      *     AlgorithmIdentifier</a>Like WebIDL does, values that are not an object are converted to a string.
983      * @param algorithm the algorithm identifier (String or Scriptable with name property)
984      * @return the resolved algorithm name
985      * @throws EcmaError (TypeError) if the algorithm object has no name
986      */
987     static String resolveAlgorithmName(final Object algorithm) {
988         if (algorithm instanceof Scriptable obj) {
989             return toRegisteredName(JavaScriptEngine.toString(getRequiredMember(obj, "name")));
990         }
991         return toRegisteredName(JavaScriptEngine.toString(algorithm));
992     }
993 
994     private static String toRegisteredName(final String name) {
995         return REGISTERED_ALGORITHM_NAMES.getOrDefault(toAsciiLowerCase(name), name);
996     }
997 
998     private static String toAsciiLowerCase(final String value) {
999         final char[] chars = value.toCharArray();
1000         for (int i = 0; i < chars.length; i++) {
1001             if (chars[i] >= 'A' && chars[i] <= 'Z') {
1002                 chars[i] = (char) (chars[i] + ('a' - 'A'));
1003             }
1004         }
1005         return new String(chars);
1006     }
1007 
1008     /**
1009      * Converts ArrayBuffer or ArrayBufferView to a ByteBuffer.
1010      * @param data the buffer source object
1011      * @return the ByteBuffer wrapping the data
1012      * @throws EcmaError (TypeError) if data is not an ArrayBuffer or ArrayBufferView
1013      */
1014     static ByteBuffer asByteBuffer(final Object data) {
1015         if (data instanceof NativeArrayBuffer nativeBuffer) {
1016             // get a copy of the bytes held by the buffer source: a detached buffer holds no bytes
1017             if (nativeBuffer.isDetached()) {
1018                 return ByteBuffer.allocate(0);
1019             }
1020             return nativeBuffer.getByteBuffer();
1021         }
1022         else if (data instanceof NativeArrayBufferView arrayBufferView) {
1023             // getByteLength() still reports the original length for a view on a detached buffer
1024             if (arrayBufferView.getBuffer().isDetached()) {
1025                 return ByteBuffer.allocate(0);
1026             }
1027             return arrayBufferView.getBuffer()
1028                     .getByteBuffer()
1029                     .position(arrayBufferView.getByteOffset())
1030                     .limit(arrayBufferView.getByteOffset() + arrayBufferView.getByteLength())
1031                     .slice();
1032         }
1033         else {
1034             throw JavaScriptEngine.typeError(
1035                     "Argument could not be converted to any of: ArrayBufferView, ArrayBuffer.");
1036         }
1037     }
1038 
1039     /**
1040      * Returns the algorithm parameters; an algorithm that requires parameters
1041      * can't be given as a plain name.
1042      * @param algorithm the algorithm identifier
1043      * @param algorithmName the resolved algorithm name
1044      * @return the algorithm parameters
1045      * @throws EcmaError (TypeError) if the algorithm is not an object
1046      */
1047     static Scriptable toAlgorithmParams(final Object algorithm, final String algorithmName) {
1048         if (algorithm instanceof Scriptable params) {
1049             return params;
1050         }
1051         throw JavaScriptEngine.typeError(algorithmName + ": the algorithm parameters are missing");
1052     }
1053 
1054     /**
1055      * Returns a required member of an algorithm dictionary.
1056      * @param dictionary the dictionary
1057      * @param member the member name
1058      * @return the value
1059      * @throws EcmaError (TypeError) if the member is missing (like WebIDL does)
1060      */
1061     static Object getRequiredMember(final Scriptable dictionary, final String member) {
1062         final Object value = ScriptableObject.getProperty(dictionary, member);
1063         if (value == Scriptable.NOT_FOUND || JavaScriptEngine.isUndefined(value)) {
1064             throw JavaScriptEngine.typeError("The required member '" + member + "' is missing");
1065         }
1066         return value;
1067     }
1068 
1069     /**
1070      * Returns a required numeric member of an algorithm dictionary.
1071      * @param dictionary the dictionary
1072      * @param member the member name
1073      * @return the value
1074      * @throws EcmaError (TypeError) if the member is missing or not a finite number
1075      */
1076     static double getRequiredNumber(final Scriptable dictionary, final String member) {
1077         final double value = JavaScriptEngine.toNumber(getRequiredMember(dictionary, member));
1078         if (Double.isNaN(value) || Double.isInfinite(value)) {
1079             throw JavaScriptEngine.typeError("The member '" + member + "' is not a finite number");
1080         }
1081         return value;
1082     }
1083 
1084     /**
1085      * Returns a required BufferSource member of an algorithm dictionary as byte array.
1086      * @param dictionary the dictionary
1087      * @param member the member name
1088      * @return the bytes
1089      * @throws EcmaError (TypeError) if the member is missing or not a BufferSource
1090      */
1091     static byte[] getRequiredBuffer(final Scriptable dictionary, final String member) {
1092         return toByteArray(asByteBuffer(getRequiredMember(dictionary, member)));
1093     }
1094 
1095     /**
1096      * Creates a NativeArrayBuffer with proper scope and prototype from the given bytes.
1097      * @param data the byte array to wrap
1098      * @return the new NativeArrayBuffer
1099      */
1100     NativeArrayBuffer createArrayBuffer(final byte[] data) {
1101         final NativeArrayBuffer buffer = new NativeArrayBuffer(data.length);
1102         buffer.getByteBuffer().put(data);
1103         buffer.setParentScope(getParentScope());
1104         buffer.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), buffer.getClassName()));
1105         return buffer;
1106     }
1107 
1108     /**
1109      * Converts the keyUsages argument like WebIDL converts a {@code sequence<KeyUsage>}.
1110      * @param keyUsages the JS usages (any iterable)
1111      * @return the usages
1112      * @throws EcmaError (TypeError) if keyUsages is not iterable or contains an unknown usage
1113      */
1114     private List<String> toKeyUsages(final Object keyUsages) {
1115         final List<String> usages = new ArrayList<>();
1116         final boolean isSequence = keyUsages instanceof Scriptable scriptable
1117                 && JavaScriptEngine.iterate(Context.getCurrentContext(), getParentScope(), scriptable, value -> {
1118                     final String usage = JavaScriptEngine.toString(value);
1119                     if (!RECOGNIZED_KEY_USAGES.contains(usage)) {
1120                         throw JavaScriptEngine.typeError("'" + usage + "' is not a valid key usage");
1121                     }
1122                     usages.add(usage);
1123                 });
1124         if (!isSequence) {
1125             throw JavaScriptEngine.typeError("The key usages are not a sequence");
1126         }
1127         return usages;
1128     }
1129 
1130     /**
1131      * Validates the key usages against the algorithm's supported operations.
1132      * @param algorithm the algorithm name
1133      * @param keyUsages the usages (see {@link #toKeyUsages(Object)})
1134      * @return the usages without duplicates, in canonical order
1135      * @throws IllegalArgumentException if a usage is not supported by the algorithm
1136      */
1137     static List<String> resolveKeyUsages(final String algorithm, final List<String> keyUsages) {
1138         for (final String usage : keyUsages) {
1139             final Set<String> supportedAlgorithms = OPERATION_TO_SUPPORTED_ALGORITHMS.get(usage);
1140             if (supportedAlgorithms == null || !supportedAlgorithms.contains(algorithm)) {
1141                 throw new IllegalArgumentException("An invalid or illegal string was specified");
1142             }
1143         }
1144 
1145         // maintain canonical ordering per RECOGNIZED_KEY_USAGES
1146         final List<String> sortedKeyUsages = new ArrayList<>();
1147         for (final String keyUsage : RECOGNIZED_KEY_USAGES) {
1148             if (keyUsages.contains(keyUsage)) {
1149                 sortedKeyUsages.add(keyUsage);
1150             }
1151         }
1152 
1153         return sortedKeyUsages;
1154     }
1155 
1156     /**
1157      * Extracts the internal Java key from a CryptoKey, validating it is the expected type.
1158      * @param <T> the expected key type
1159      * @param cryptoKey the CryptoKey
1160      * @param expectedKeyType the expected class (e.g. SecretKey.class)
1161      * @return the internal key cast to the expected type
1162      * @throws InvalidAccessException if the key is not the expected type
1163      */
1164     static <T extends Key> T getInternalKey(final CryptoKey cryptoKey, final Class<T> expectedKeyType) {
1165         final Key internalKey = cryptoKey.getInternalKey();
1166         if (!expectedKeyType.isInstance(internalKey)) {
1167             throw new InvalidAccessException("A parameter or an operation is not supported by the underlying object");
1168         }
1169         return expectedKeyType.cast(internalKey);
1170     }
1171 }