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.nio.ByteBuffer;
18  import java.security.GeneralSecurityException;
19  import java.security.Key;
20  import java.security.KeyPair;
21  import java.security.KeyPairGenerator;
22  import java.security.MessageDigest;
23  import java.security.PrivateKey;
24  import java.security.PublicKey;
25  import java.security.Signature;
26  import java.security.spec.AlgorithmParameterSpec;
27  import java.security.spec.ECGenParameterSpec;
28  import java.security.spec.MGF1ParameterSpec;
29  import java.security.spec.PSSParameterSpec;
30  import java.security.spec.RSAKeyGenParameterSpec;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.LinkedHashSet;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  
39  import javax.crypto.Cipher;
40  import javax.crypto.KeyGenerator;
41  import javax.crypto.Mac;
42  import javax.crypto.SecretKey;
43  import javax.crypto.spec.GCMParameterSpec;
44  import javax.crypto.spec.IvParameterSpec;
45  import javax.crypto.spec.OAEPParameterSpec;
46  import javax.crypto.spec.PSource;
47  import javax.crypto.spec.SecretKeySpec;
48  
49  import org.htmlunit.corejs.javascript.EcmaError;
50  import org.htmlunit.corejs.javascript.NativePromise;
51  import org.htmlunit.corejs.javascript.Scriptable;
52  import org.htmlunit.corejs.javascript.ScriptableObject;
53  import org.htmlunit.corejs.javascript.VarScope;
54  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
55  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
56  import org.htmlunit.javascript.HtmlUnitScriptable;
57  import org.htmlunit.javascript.JavaScriptEngine;
58  import org.htmlunit.javascript.configuration.JsxClass;
59  import org.htmlunit.javascript.configuration.JsxConstructor;
60  import org.htmlunit.javascript.configuration.JsxFunction;
61  import org.htmlunit.javascript.host.dom.DOMException;
62  
63  /**
64   * A JavaScript object for {@code SubtleCrypto}.
65   *
66   * @author Ahmed Ashour
67   * @author Ronald Brill
68   * @author Atsushi Nakagawa
69   * @author Lai Quang Duong
70   */
71  @JsxClass
72  public class SubtleCrypto extends HtmlUnitScriptable {
73  
74      /**
75       * Maps each crypto operation to its supported algorithm names.
76       * @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
77       */
78      private static final Map<String, Set<String>> OPERATION_TO_SUPPORTED_ALGORITHMS = Map.ofEntries(
79              Map.entry("encrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
80              Map.entry("decrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
81              Map.entry("sign", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
82              Map.entry("verify", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
83              Map.entry("digest", Set.of("SHA-1", "SHA-256", "SHA-384", "SHA-512")),
84              Map.entry("generateKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP",
85                      "ECDSA", "ECDH", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC")),
86              Map.entry("importKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP", "ECDSA", "ECDH",
87                      "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC", "HKDF", "PBKDF2")),
88              Map.entry("wrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
89              Map.entry("unwrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
90              Map.entry("deriveBits", Set.of("ECDH", "HKDF", "PBKDF2")),
91              Map.entry("deriveKey", Set.of("ECDH", "HKDF", "PBKDF2"))
92      );
93  
94      /**
95       * @see <a href="https://w3c.github.io/webcrypto/#dfn-RecognizedKeyUsage">RecognizedKeyUsage</a>
96       */
97      private static final Set<String> RECOGNIZED_KEY_USAGES = Collections.unmodifiableSet(
98              new LinkedHashSet<>(List.of("encrypt", "decrypt", "sign", "verify",
99                      "deriveKey", "deriveBits", "wrapKey", "unwrapKey")));
100 
101     /**
102      * @see <a href="https://w3c.github.io/webcrypto/#aes-gcm-operations">AES-GCM encrypt, step 6</a>
103      */
104     private static final Set<Integer> VALID_AES_GCM_TAG_LENGTHS = Set.of(32, 64, 96, 104, 112, 120, 128);
105 
106     private static class InvalidAccessException extends RuntimeException {
107         InvalidAccessException(final String message) {
108             super(message);
109         }
110     }
111 
112     /**
113      * Creates an instance.
114      */
115     @JsxConstructor
116     public void jsConstructor() {
117         throw JavaScriptEngine.typeErrorIllegalConstructor();
118     }
119 
120     private NativePromise notImplemented() {
121         return setupRejectedPromise(() ->
122                 new DOMException("Operation is not supported", DOMException.NOT_SUPPORTED_ERR));
123     }
124 
125     /**
126      * Encrypts data using the given key and algorithm.
127      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-encrypt">SubtleCrypto.encrypt()</a>
128      * @param algorithm the algorithm identifier with parameters
129      * @param key the CryptoKey to encrypt with
130      * @param data the data to encrypt
131      * @return a Promise that fulfills with an ArrayBuffer containing the ciphertext
132      */
133     @JsxFunction
134     public NativePromise encrypt(final Object algorithm, final CryptoKey key, final Object data) {
135         return doCipher(algorithm, key, data, Cipher.ENCRYPT_MODE);
136     }
137 
138     /**
139      * Decrypts data using the given key and algorithm.
140      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-decrypt">SubtleCrypto.decrypt()</a>
141      * @param algorithm the algorithm identifier with parameters
142      * @param key the CryptoKey to decrypt with
143      * @param data the data to decrypt
144      * @return a Promise that fulfills with an ArrayBuffer containing the plaintext
145      */
146     @JsxFunction
147     public NativePromise decrypt(final Object algorithm, final CryptoKey key, final Object data) {
148         return doCipher(algorithm, key, data, Cipher.DECRYPT_MODE);
149     }
150 
151     /**
152      * Shared encrypt/decrypt implementation.
153      */
154     private NativePromise doCipher(final Object algorithm, final CryptoKey key,
155             final Object data, final int cipherMode) {
156         final String operation = switch (cipherMode) {
157             case Cipher.ENCRYPT_MODE -> "encrypt";
158             case Cipher.DECRYPT_MODE -> "decrypt";
159             default -> throw new IllegalArgumentException("Invalid cipher mode: " + cipherMode);
160         };
161 
162         final byte[] result;
163         try {
164             final String algorithmName = resolveAlgorithmName(algorithm);
165             ensureAlgorithmIsSupported(operation, algorithmName);
166             ensureKeyAlgorithmMatches(algorithmName, key);
167             ensureKeyUsage(key, operation);
168 
169             final ByteBuffer inputData = asByteBuffer(data);
170 
171             // encrypt/decrypt requires algorithm parameters as an object (iv, counter, etc.)
172             if (!(algorithm instanceof Scriptable algorithmObj)) {
173                 throw new IllegalArgumentException("An invalid or illegal string was specified");
174             }
175 
176             switch (algorithmName) {
177                 case "AES-CBC": {
178                     // https://w3c.github.io/webcrypto/#aes-cbc-operations
179                     final byte[] iv = extractBuffer(algorithmObj, "iv");
180                     if (iv == null || iv.length != 16) {
181                         throw new IllegalArgumentException(
182                                 "Data provided to an operation does not meet requirements");
183                     }
184                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
185                     final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
186                     cipher.init(cipherMode, secretKey, new IvParameterSpec(iv));
187                     result = cipher.doFinal(toByteArray(inputData));
188                     break;
189                 }
190                 case "AES-GCM": {
191                     // https://w3c.github.io/webcrypto/#aes-gcm-operations
192                     final byte[] iv = extractBuffer(algorithmObj, "iv");
193                     if (iv == null || iv.length == 0) {
194                         throw new IllegalArgumentException(
195                                 "Data provided to an operation does not meet requirements");
196                     }
197 
198                     final int tagLength;
199                     final Object tagLengthProp = ScriptableObject.getProperty(algorithmObj, "tagLength");
200                     if (tagLengthProp instanceof Number num) {
201                         tagLength = num.intValue();
202                         if (!VALID_AES_GCM_TAG_LENGTHS.contains(tagLength)) {
203                             throw new IllegalArgumentException(
204                                     "Data provided to an operation does not meet requirements");
205                         }
206                     }
207                     else {
208                         tagLength = 128;
209                     }
210 
211                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
212                     final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
213                     cipher.init(cipherMode, secretKey, new GCMParameterSpec(tagLength, iv));
214 
215                     final Object aadProp = ScriptableObject.getProperty(algorithmObj, "additionalData");
216                     if (aadProp instanceof Scriptable) {
217                         final ByteBuffer aad = asByteBuffer(aadProp);
218                         cipher.updateAAD(toByteArray(aad));
219                     }
220 
221                     result = cipher.doFinal(toByteArray(inputData));
222                     break;
223                 }
224                 case "AES-CTR": {
225                     // https://w3c.github.io/webcrypto/#aes-ctr-operations
226                     final byte[] counter = extractBuffer(algorithmObj, "counter");
227                     if (counter == null || counter.length != 16) {
228                         throw new IllegalArgumentException(
229                                 "Data provided to an operation does not meet requirements");
230                     }
231 
232                     final Object lengthProp = ScriptableObject.getProperty(algorithmObj, "length");
233                     if (!(lengthProp instanceof Number numLength)) {
234                         throw new IllegalArgumentException(
235                                 "Data provided to an operation does not meet requirements");
236                     }
237                     final int counterLength = numLength.intValue();
238                     if (counterLength < 1 || counterLength > 128) {
239                         throw new IllegalArgumentException(
240                                 "Data provided to an operation does not meet requirements");
241                     }
242 
243                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
244                     // Java always increments the full 128-bit counter, ignoring the 'length' partitioning.
245                     // This only becomes an issue when data exceeds 2^length AES blocks (16 bytes each),
246                     // but in real-world usage (length >= 64) it's pretty much unreachable.
247                     final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
248                     cipher.init(cipherMode, secretKey, new IvParameterSpec(counter));
249                     result = cipher.doFinal(toByteArray(inputData));
250                     break;
251                 }
252                 case "RSA-OAEP": {
253                     // https://w3c.github.io/webcrypto/#rsa-oaep-operations
254                     final Scriptable keyAlgorithm = key.getAlgorithm();
255                     final Object hashObj = ScriptableObject.getProperty(keyAlgorithm, "hash");
256                     final String hash = resolveAlgorithmName(hashObj);
257 
258                     final byte[] label;
259                     final Object labelProp = ScriptableObject.getProperty(algorithmObj, "label");
260                     if (labelProp instanceof Scriptable) {
261                         final ByteBuffer labelBuf = asByteBuffer(labelProp);
262                         label = toByteArray(labelBuf);
263                     }
264                     else {
265                         label = new byte[0];
266                     }
267 
268                     final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hash);
269                     final AlgorithmParameterSpec oaepSpec = new OAEPParameterSpec(
270                             hash, "MGF1", mgf1Spec, new PSource.PSpecified(label));
271 
272                     final Key internalKey;
273                     if (cipherMode == Cipher.ENCRYPT_MODE) {
274                         internalKey = getInternalKey(key, PublicKey.class);
275                     }
276                     else {
277                         internalKey = getInternalKey(key, PrivateKey.class);
278                     }
279 
280                     final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
281                     cipher.init(cipherMode, internalKey, oaepSpec);
282                     result = cipher.doFinal(toByteArray(inputData));
283                     break;
284                 }
285                 default:
286                     throw new UnsupportedOperationException(operation + " " + algorithmName);
287             }
288         }
289         catch (final EcmaError e) {
290             return setupRejectedPromise(() -> e);
291         }
292         catch (final InvalidAccessException e) {
293             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.INVALID_ACCESS_ERR));
294         }
295         catch (final IllegalArgumentException e) {
296             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
297         }
298         catch (final GeneralSecurityException | UnsupportedOperationException e) {
299             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
300                     DOMException.NOT_SUPPORTED_ERR));
301         }
302         return setupPromise(() -> createArrayBuffer(result));
303     }
304 
305     /**
306      * Signs data using the given key.
307      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-sign">SubtleCrypto.sign()</a>
308      * @param algorithm the algorithm identifier (String or object with name property)
309      * @param key the CryptoKey to sign with
310      * @param data the data to sign
311      * @return a Promise that fulfills with an ArrayBuffer containing the signature
312      */
313     @JsxFunction
314     public NativePromise sign(final Object algorithm, final CryptoKey key, final Object data) {
315         return doSignOrVerify(algorithm, key, null, data, true);
316     }
317 
318     /**
319      * Verifies a signature using the given key.
320      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify">SubtleCrypto.verify()</a>
321      * @param algorithm the algorithm identifier (String or object with name property)
322      * @param key the CryptoKey to verify with
323      * @param signature the signature to verify
324      * @param data the data that was signed
325      * @return a Promise that fulfills with a boolean indicating whether the signature is valid
326      */
327     @JsxFunction
328     public NativePromise verify(final Object algorithm, final CryptoKey key,
329             final Object signature, final Object data) {
330         return doSignOrVerify(algorithm, key, signature, data, false);
331     }
332 
333     /**
334      * Shared sign/verify implementation.
335      */
336     private NativePromise doSignOrVerify(final Object algorithm, final CryptoKey key,
337             final Object existingSignature, final Object data, final boolean isSigning) {
338         final Object result;
339         try {
340             final String algorithmName = resolveAlgorithmName(algorithm);
341             final String operation = isSigning ? "sign" : "verify";
342             ensureAlgorithmIsSupported(operation, algorithmName);
343             ensureKeyAlgorithmMatches(algorithmName, key);
344             ensureKeyUsage(key, operation);
345 
346             final ByteBuffer inputData = asByteBuffer(data);
347 
348             switch (algorithmName) {
349                 case "HMAC": {
350                     // https://w3c.github.io/webcrypto/#hmac-operations
351                     final SecretKey secretKey = getInternalKey(key, SecretKey.class);
352                     final Mac mac = Mac.getInstance(secretKey.getAlgorithm());
353                     mac.init(secretKey);
354                     mac.update(inputData);
355                     final byte[] macBytes = mac.doFinal();
356                     if (isSigning) {
357                         result = macBytes;
358                     }
359                     else {
360                         result = MessageDigest.isEqual(macBytes,
361                                 toByteArray(asByteBuffer(existingSignature)));
362                     }
363                     break;
364                 }
365                 case "RSASSA-PKCS1-v1_5":
366                     // https://w3c.github.io/webcrypto/#rsassa-pkcs1
367                 case "RSA-PSS":
368                     // https://w3c.github.io/webcrypto/#rsa-pss
369                 case "ECDSA": {
370                     // https://w3c.github.io/webcrypto/#ecdsa-operations
371                     final Signature sig = "ECDSA".equals(algorithmName)
372                             ? resolveEcdsaSignature(algorithm)
373                             : resolveRsaSignature(algorithmName, algorithm, key);
374                     if (isSigning) {
375                         sig.initSign(getInternalKey(key, PrivateKey.class));
376                         sig.update(inputData);
377                         result = sig.sign();
378                     }
379                     else {
380                         sig.initVerify(getInternalKey(key, PublicKey.class));
381                         sig.update(inputData);
382                         result = sig.verify(toByteArray(asByteBuffer(existingSignature)));
383                     }
384                     break;
385                 }
386                 default:
387                     throw new UnsupportedOperationException(operation + " " + algorithmName);
388             }
389         }
390         catch (final EcmaError e) {
391             return setupRejectedPromise(() -> e);
392         }
393         catch (final InvalidAccessException e) {
394             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.INVALID_ACCESS_ERR));
395         }
396         catch (final IllegalArgumentException e) {
397             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
398         }
399         catch (final GeneralSecurityException | UnsupportedOperationException e) {
400             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
401                     DOMException.NOT_SUPPORTED_ERR));
402         }
403 
404         if (isSigning) {
405             return setupPromise(() -> createArrayBuffer((byte[]) result));
406         }
407         return setupPromise(() -> result);
408     }
409 
410     /**
411      * Resolves the RSA {@link Signature} instance for the given algorithm.
412      */
413     private static Signature resolveRsaSignature(final String algorithmName, final Object algorithmParams,
414             final CryptoKey key) throws GeneralSecurityException {
415         final Object hashObj = ScriptableObject.getProperty(key.getAlgorithm(), "hash");
416         final String hash = resolveAlgorithmName(hashObj);
417         final String javaHash = hash.replace("-", "");
418 
419         if ("RSASSA-PKCS1-v1_5".equals(algorithmName)) {
420             return Signature.getInstance(javaHash + "withRSA");
421         }
422 
423         if (!(algorithmParams instanceof Scriptable obj)) {
424             throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
425         }
426         final Object saltLengthProp = ScriptableObject.getProperty(obj, "saltLength");
427         if (!(saltLengthProp instanceof Number num)) {
428             throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
429         }
430         final int saltLength = num.intValue();
431 
432         final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hash);
433         final PSSParameterSpec pssSpec = new PSSParameterSpec(hash, "MGF1", mgf1Spec, saltLength, 1);
434         final Signature sig = Signature.getInstance("RSASSA-PSS");
435         sig.setParameter(pssSpec);
436         return sig;
437     }
438 
439     /**
440      * Resolves the ECDSA {@link Signature} instance for the given algorithm params.
441      */
442     private static Signature resolveEcdsaSignature(final Object algorithmParams)
443             throws GeneralSecurityException {
444         if (!(algorithmParams instanceof Scriptable obj)) {
445             throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
446         }
447         final Object hashProp = ScriptableObject.getProperty(obj, "hash");
448         final String hash = resolveAlgorithmName(hashProp);
449         final String javaHash = hash.replace("-", "");
450         return Signature.getInstance(javaHash + "withECDSAinP1363Format");
451     }
452 
453     private static byte[] toByteArray(final ByteBuffer buffer) {
454         final byte[] result = new byte[buffer.remaining()];
455         buffer.get(result);
456         return result;
457     }
458 
459     /**
460      * Generates a digest of the given data.
461      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest">SubtleCrypto.digest()</a>
462      * @param hashAlgorithm a string or an object with a single property name containing the hash algorithm to use
463      * @param data an object containing the data to be digested
464      * @return a Promise that fulfills with an ArrayBuffer containing the digest
465      */
466     @JsxFunction
467     public NativePromise digest(final Object hashAlgorithm, final Object data) {
468         final byte[] digest;
469         try {
470             final ByteBuffer inputData = asByteBuffer(data);
471             final String algorithm = resolveAlgorithmName(hashAlgorithm);
472             ensureAlgorithmIsSupported("digest", algorithm);
473 
474             final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
475             messageDigest.update(inputData);
476             digest = messageDigest.digest();
477         }
478         catch (final EcmaError e) {
479             return setupRejectedPromise(() -> e);
480         }
481         catch (final IllegalArgumentException e) {
482             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
483         }
484         catch (final GeneralSecurityException | UnsupportedOperationException e) {
485             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
486                     DOMException.NOT_SUPPORTED_ERR));
487         }
488         return setupPromise(() -> createArrayBuffer(digest));
489     }
490 
491     /**
492      * Generates a new key (for symmetric algorithms) or key pair (for public-key algorithms).
493      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-generateKey">SubtleCrypto.generateKey()</a>
494      * @param keyGenParams algorithm-specific key generation parameters
495      * @param isExtractable whether the key(s) can be exported
496      * @param keyUsages permitted operations for the key(s)
497      * @return a Promise that fulfills with a CryptoKey or CryptoKeyPair
498      */
499     @JsxFunction
500     public NativePromise generateKey(final Scriptable keyGenParams, final boolean isExtractable,
501             final Scriptable keyUsages) {
502         final Object result;
503         try {
504             final String algorithm = resolveAlgorithmName(keyGenParams);
505             ensureAlgorithmIsSupported("generateKey", algorithm);
506 
507             final VarScope scope = keyGenParams.getParentScope();
508 
509             switch (algorithm) {
510                 case "RSASSA-PKCS1-v1_5":
511                 case "RSA-PSS":
512                 case "RSA-OAEP": {
513                     final RsaHashedKeyAlgorithm rsaParams = RsaHashedKeyAlgorithm.from(keyGenParams);
514                     final List<String> usages = resolveKeyUsages(algorithm, keyUsages);
515 
516                     final KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
517                     keyPairGen.initialize(new RSAKeyGenParameterSpec(
518                             rsaParams.getModulusLength(), rsaParams.getPublicExponentAsBigInteger()));
519                     final KeyPair keyPair = keyPairGen.generateKeyPair();
520 
521                     final Scriptable algoObj = rsaParams.toScriptableObject(scope);
522                     result = createKeyPair(keyPair, algoObj, isExtractable, usages, scope);
523                     break;
524                 }
525                 case "ECDSA":
526                 case "ECDH": {
527                     final EcKeyAlgorithm ecParams = EcKeyAlgorithm.from(keyGenParams);
528                     final List<String> usages = resolveKeyUsages(algorithm, keyUsages);
529 
530                     final KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
531                     keyPairGen.initialize(new ECGenParameterSpec(ecParams.getJavaCurveName()));
532                     final KeyPair keyPair = keyPairGen.generateKeyPair();
533 
534                     final Scriptable algoObj = ecParams.toScriptableObject(scope);
535                     result = createKeyPair(keyPair, algoObj, isExtractable, usages, scope);
536                     break;
537                 }
538                 case "AES-CBC":
539                 case "AES-CTR":
540                 case "AES-GCM":
541                 case "AES-KW": {
542                     final AesKeyAlgorithm aesParams = AesKeyAlgorithm.from(keyGenParams);
543                     final List<String> usages = resolveKeyUsages(algorithm, keyUsages);
544                     if (usages.isEmpty()) {
545                         throw new IllegalArgumentException("An invalid or illegal string was specified");
546                     }
547 
548                     final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
549                     keyGen.init(aesParams.getLength());
550                     final SecretKey secretKey = keyGen.generateKey();
551 
552                     final Scriptable algoObj = aesParams.toScriptableObject(scope);
553                     result = CryptoKey.create(getParentScope(), secretKey, isExtractable, algoObj, usages);
554                     break;
555                 }
556                 case "HMAC": {
557                     final HmacKeyAlgorithm hmacParams = HmacKeyAlgorithm.from(keyGenParams);
558                     final List<String> usages = resolveKeyUsages("HMAC", keyUsages);
559                     if (usages.isEmpty()) {
560                         throw new IllegalArgumentException("An invalid or illegal string was specified");
561                     }
562 
563                     final KeyGenerator keyGen = KeyGenerator.getInstance(hmacParams.getJavaName());
564                     keyGen.init(hmacParams.getLength());
565                     final SecretKey secretKey = keyGen.generateKey();
566 
567                     final Scriptable algoObj = hmacParams.toScriptableObject(scope);
568                     result = CryptoKey.create(getParentScope(), secretKey, isExtractable, algoObj, usages);
569                     break;
570                 }
571                 default:
572                     throw new UnsupportedOperationException("generateKey " + algorithm);
573             }
574         }
575         catch (final EcmaError e) {
576             return setupRejectedPromise(() -> e);
577         }
578         catch (final IllegalArgumentException e) {
579             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
580         }
581         catch (final GeneralSecurityException | UnsupportedOperationException e) {
582             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
583                     DOMException.NOT_SUPPORTED_ERR));
584         }
585         return setupPromise(() -> result);
586     }
587 
588     /**
589      * Creates a CryptoKeyPair (plain JS object with publicKey/privateKey) from a Java KeyPair.
590      * The public key is always extractable regardless of the extractable parameter.
591      * Usages are split: public gets {encrypt,verify,wrapKey},
592      * private gets {decrypt,sign,unwrapKey,deriveBits,deriveKey}.
593      */
594     private Scriptable createKeyPair(final KeyPair keyPair, final Scriptable algoObj,
595             final boolean isExtractable, final List<String> allUsages, final VarScope scope) {
596         final Set<String> publicUsageSet = Set.of("encrypt", "verify", "wrapKey");
597         final Set<String> privateUsageSet = Set.of("decrypt", "sign", "unwrapKey", "deriveBits", "deriveKey");
598 
599         final List<String> publicUsages = new ArrayList<>();
600         final List<String> privateUsages = new ArrayList<>();
601         for (final String usage : allUsages) {
602             if (publicUsageSet.contains(usage)) {
603                 publicUsages.add(usage);
604             }
605             if (privateUsageSet.contains(usage)) {
606                 privateUsages.add(usage);
607             }
608         }
609 
610         // if privateKey usages would be empty, throw SyntaxError
611         if (privateUsages.isEmpty()) {
612             throw new IllegalArgumentException("An invalid or illegal string was specified");
613         }
614 
615         // public key is always extractable
616         final CryptoKey publicKey = CryptoKey.create(
617                 getParentScope(), keyPair.getPublic(), true, algoObj, publicUsages);
618         final CryptoKey privateKey = CryptoKey.create(
619                 getParentScope(), keyPair.getPrivate(), isExtractable, algoObj, privateUsages);
620 
621         final Scriptable keyPairObj = JavaScriptEngine.newObject(scope);
622         ScriptableObject.putProperty(keyPairObj, "publicKey", publicKey);
623         ScriptableObject.putProperty(keyPairObj, "privateKey", privateKey);
624         return keyPairObj;
625     }
626 
627     /**
628      * Not yet implemented.
629      *
630      * @return a Promise which will be fulfilled with a CryptoKey object representing the new key
631      */
632     @JsxFunction
633     public NativePromise deriveKey() {
634         return notImplemented();
635     }
636 
637     /**
638      * Not yet implemented.
639      *
640      * @return a Promise which will be fulfilled with an ArrayBuffer containing the derived bits
641      */
642     @JsxFunction
643     public NativePromise deriveBits() {
644         return notImplemented();
645     }
646 
647     /**
648      * Imports a key from external, portable key material.
649      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-importKey">SubtleCrypto.importKey()</a>
650      * @param format the data format ("raw", "pkcs8", "spki", "jwk")
651      * @param keyData the key material (BufferSource for raw/pkcs8/spki, JsonWebKey for jwk)
652      * @param keyImportParams algorithm-specific import parameters
653      * @param isExtractable whether the key can be exported
654      * @param keyUsages permitted operations for this key
655      * @return a Promise that fulfills with the imported CryptoKey
656      */
657     @JsxFunction
658     public NativePromise importKey(final String format, final Scriptable keyData,
659             final Scriptable keyImportParams, final boolean isExtractable, final Scriptable keyUsages) {
660         final CryptoKey key;
661         try {
662             final String algorithm = resolveAlgorithmName(keyImportParams);
663             ensureAlgorithmIsSupported("importKey", algorithm);
664 
665             switch (format) {
666                 case "raw":
667                     key = importRawKey(algorithm, keyData, keyImportParams, isExtractable, keyUsages);
668                     break;
669                 case "pkcs8":
670                 case "spki":
671                 case "jwk":
672                     return notImplemented();
673                 default:
674                     throw new IllegalArgumentException("An invalid or illegal string was specified");
675             }
676         }
677         catch (final EcmaError e) {
678             return setupRejectedPromise(() -> e);
679         }
680         catch (final IllegalArgumentException e) {
681             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
682         }
683         catch (final UnsupportedOperationException e) {
684             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
685                     DOMException.NOT_SUPPORTED_ERR));
686         }
687         return setupPromise(() -> key);
688     }
689 
690     private CryptoKey importRawKey(final String algorithm, final Scriptable keyData,
691             final Scriptable keyImportParams, final boolean isExtractable, final Scriptable keyUsages) {
692         final ByteBuffer byteBuffer = asByteBuffer(keyData);
693         final byte[] rawBytes = new byte[byteBuffer.remaining()];
694         byteBuffer.get(rawBytes);
695         final int bitLength = rawBytes.length * 8;
696         if (bitLength == 0) {
697             throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
698         }
699 
700         final List<String> usages = resolveKeyUsages(algorithm, keyUsages);
701         if (usages.isEmpty()) {
702             throw new IllegalArgumentException("An invalid or illegal string was specified");
703         }
704 
705         if ("HMAC".equals(algorithm)) {
706             final HmacKeyAlgorithm params = HmacKeyAlgorithm.from(keyImportParams, bitLength);
707             final int length = params.getLength();
708             if (length > bitLength || length <= bitLength - 8) {
709                 throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
710             }
711 
712             final Scriptable scriptableAlgorithm = params.toScriptableObject(keyImportParams.getParentScope());
713             final SecretKey internalKey = new SecretKeySpec(rawBytes, params.getJavaName());
714             return CryptoKey.create(getParentScope(), internalKey, isExtractable, scriptableAlgorithm, usages);
715         }
716 
717         if (AesKeyAlgorithm.isSupported(algorithm)) {
718             final AesKeyAlgorithm aesAlgo = new AesKeyAlgorithm(algorithm, bitLength);
719             final Scriptable scriptableAlgorithm = aesAlgo.toScriptableObject(keyImportParams.getParentScope());
720             final SecretKey internalKey = new SecretKeySpec(rawBytes, "AES");
721             return CryptoKey.create(getParentScope(), internalKey, isExtractable, scriptableAlgorithm, usages);
722         }
723 
724         throw new UnsupportedOperationException("importKey raw " + algorithm);
725     }
726 
727     /**
728      * Exports a key in the specified format.
729      * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-exportKey">SubtleCrypto.exportKey()</a>
730      * @param format the data format ("raw", "pkcs8", "spki", "jwk")
731      * @param key the CryptoKey to export
732      * @return a Promise that fulfills with the key data
733      */
734     @JsxFunction
735     public NativePromise exportKey(final String format, final CryptoKey key) {
736         final byte[] result;
737         try {
738             if (!key.getExtractable()) {
739                 return setupRejectedPromise(() -> new DOMException(
740                         "A parameter or an operation is not supported by the underlying object",
741                         DOMException.INVALID_ACCESS_ERR));
742             }
743 
744             switch (format) {
745                 case "raw": {
746                     if (!(key.getInternalKey() instanceof SecretKey secretKey)) {
747                         throw new IllegalArgumentException(
748                                 "Data provided to an operation does not meet requirements");
749                     }
750                     result = secretKey.getEncoded();
751                     break;
752                 }
753                 case "pkcs8":
754                 case "spki":
755                 case "jwk":
756                     return notImplemented();
757                 default:
758                     throw new IllegalArgumentException("An invalid or illegal string was specified");
759             }
760         }
761         catch (final IllegalArgumentException e) {
762             return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
763         }
764         catch (final UnsupportedOperationException e) {
765             return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
766                     DOMException.NOT_SUPPORTED_ERR));
767         }
768         return setupPromise(() -> createArrayBuffer(result));
769     }
770 
771     /**
772      * Not yet implemented.
773      *
774      * @return a Promise that fulfills with an ArrayBuffer containing the encrypted exported key
775      */
776     @JsxFunction
777     public NativePromise wrapKey() {
778         return notImplemented();
779     }
780 
781     /**
782      * Not yet implemented.
783      *
784      * @return a Promise that fulfills with the unwrapped key as a CryptoKey object
785      */
786     @JsxFunction
787     public NativePromise unwrapKey() {
788         return notImplemented();
789     }
790 
791     /**
792      * Checks if the specified crypto operation supports the given algorithm.
793      * @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
794      * @param operation the crypto operation (e.g. "digest", "sign")
795      * @param algorithm the algorithm name (e.g. "SHA-256", "HMAC")
796      * @throws UnsupportedOperationException if the operation does not support the algorithm
797      */
798     private static void ensureAlgorithmIsSupported(final String operation, final String algorithm) {
799         final Set<String> supportedAlgorithms = OPERATION_TO_SUPPORTED_ALGORITHMS.get(operation);
800         if (supportedAlgorithms == null || !supportedAlgorithms.contains(algorithm)) {
801             throw new UnsupportedOperationException(operation + " " + algorithm);
802         }
803     }
804 
805     /**
806      * Verifies that the operation's algorithm name matches the key's algorithm name.
807      * @param algorithmName the algorithm name from the operation parameters
808      * @param key the CryptoKey being used
809      * @throws InvalidAccessException if the algorithm names don't match
810      */
811     private static void ensureKeyAlgorithmMatches(final String algorithmName, final CryptoKey key) {
812         final String keyAlgoName = resolveAlgorithmName(key.getAlgorithm());
813         if (!algorithmName.equals(keyAlgoName)) {
814             throw new InvalidAccessException(
815                     "A parameter or an operation is not supported by the underlying object");
816         }
817     }
818 
819     /**
820      * Verifies that the key's usages include the specified usage.
821      * @param key the CryptoKey being used
822      * @param usage the required usage (e.g. "encrypt", "sign")
823      * @throws InvalidAccessException if the key doesn't have the required usage
824      */
825     private static void ensureKeyUsage(final CryptoKey key, final String usage) {
826         if (!key.getUsagesInternal().contains(usage)) {
827             throw new InvalidAccessException(
828                     "A parameter or an operation is not supported by the underlying object");
829         }
830     }
831 
832     /**
833      * Resolves the algorithm name from the given {@code AlgorithmIdentifier}.
834      * @see <a href="https://w3c.github.io/webcrypto/#dfn-AlgorithmIdentifier">
835      *     AlgorithmIdentifier</a>
836      * @param algorithm the algorithm identifier (String or Scriptable with name property)
837      * @return the resolved algorithm name
838      * @throws IllegalArgumentException if the identifier cannot be resolved
839      */
840     static String resolveAlgorithmName(final Object algorithm) {
841         if (algorithm instanceof String str) {
842             return str;
843         }
844         if (algorithm instanceof Scriptable obj) {
845             final Object name = ScriptableObject.getProperty(obj, "name");
846             if (name instanceof String nameStr) {
847                 return nameStr;
848             }
849         }
850         throw new IllegalArgumentException("An invalid or illegal string was specified");
851     }
852 
853     /**
854      * Converts ArrayBuffer or ArrayBufferView to a ByteBuffer.
855      * @param data the buffer source object
856      * @return the ByteBuffer wrapping the data
857      * @throws IllegalArgumentException if data is not a Scriptable or is NOT_FOUND
858      * @throws EcmaError if data is not an ArrayBuffer or ArrayBufferView
859      */
860     static ByteBuffer asByteBuffer(final Object data) {
861         if (!(data instanceof Scriptable)) {
862             throw new IllegalArgumentException("An invalid or illegal string was specified");
863         }
864         if (data == Scriptable.NOT_FOUND) {
865             throw new IllegalArgumentException("An invalid or illegal string was specified");
866         }
867         if (data instanceof NativeArrayBuffer nativeBuffer) {
868             return ByteBuffer.wrap(nativeBuffer.getBuffer());
869         }
870         else if (data instanceof NativeArrayBufferView arrayBufferView) {
871             final NativeArrayBuffer arrayBuffer = arrayBufferView.getBuffer();
872             return ByteBuffer.wrap(
873                     arrayBuffer.getBuffer(), arrayBufferView.getByteOffset(), arrayBufferView.getByteLength());
874         }
875         else {
876             throw JavaScriptEngine.typeError(
877                     "Argument could not be converted to any of: ArrayBufferView, ArrayBuffer.");
878         }
879     }
880 
881     /**
882      * Reads a property from a JS object and converts it to a byte array.
883      * @param obj the JS object containing the property
884      * @param property the property name (e.g. "iv", "counter", "label")
885      * @return the byte array, or {@code null} if the property is absent or not convertible
886      */
887     private static byte[] extractBuffer(final Scriptable obj, final String property) {
888         final Object prop = ScriptableObject.getProperty(obj, property);
889         if (prop instanceof Scriptable) {
890             final ByteBuffer buf = asByteBuffer(prop);
891             return toByteArray(buf);
892         }
893         return null;
894     }
895 
896     /**
897      * Creates a NativeArrayBuffer with proper scope and prototype from the given bytes.
898      * @param data the byte array to wrap
899      * @return the new NativeArrayBuffer
900      */
901     NativeArrayBuffer createArrayBuffer(final byte[] data) {
902         final NativeArrayBuffer buffer = new NativeArrayBuffer(data.length);
903         System.arraycopy(data, 0, buffer.getBuffer(), 0, data.length);
904         buffer.setParentScope(getParentScope());
905         buffer.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), buffer.getClassName()));
906         return buffer;
907     }
908 
909     /**
910      * Resolves and validates key usages from the JS array against the algorithm's supported operations.
911      * @param algorithm the algorithm name
912      * @param keyUsages the JS usages array
913      * @return the validated, ordered list of usages
914      * @throws IllegalArgumentException if usages array is invalid or contains unrecognized values
915      */
916     static List<String> resolveKeyUsages(final String algorithm, final Scriptable keyUsages) {
917         if (!JavaScriptEngine.isArrayLike(keyUsages)) {
918             throw new IllegalArgumentException("An invalid or illegal string was specified");
919         }
920 
921         final Set<String> supportedKeyUsages = new HashSet<>();
922         JavaScriptEngine.iterateArrayLike(null, keyUsages, usage -> {
923             if (!(usage instanceof String usageStr)) {
924                 throw new IllegalArgumentException("An invalid or illegal string was specified");
925             }
926             if (!RECOGNIZED_KEY_USAGES.contains(usageStr)) {
927                 throw new IllegalArgumentException("An invalid or illegal string was specified");
928             }
929 
930             final Set<String> supportedAlgorithms = OPERATION_TO_SUPPORTED_ALGORITHMS.get(usageStr);
931             if (supportedAlgorithms != null && supportedAlgorithms.contains(algorithm)) {
932                 supportedKeyUsages.add(usageStr);
933             }
934         });
935 
936         // maintain canonical ordering per RECOGNIZED_KEY_USAGES
937         final List<String> sortedKeyUsages = new ArrayList<>();
938         for (final String keyUsage : RECOGNIZED_KEY_USAGES) {
939             if (supportedKeyUsages.contains(keyUsage)) {
940                 sortedKeyUsages.add(keyUsage);
941             }
942         }
943 
944         return sortedKeyUsages;
945     }
946 
947     /**
948      * Extracts the internal Java key from a CryptoKey, validating it is the expected type.
949      * @param <T> the expected key type
950      * @param cryptoKey the CryptoKey
951      * @param expectedKeyType the expected class (e.g. SecretKey.class)
952      * @return the internal key cast to the expected type
953      * @throws InvalidAccessException if the key is not the expected type
954      */
955     static <T extends Key> T getInternalKey(final CryptoKey cryptoKey, final Class<T> expectedKeyType) {
956         final Key internalKey = cryptoKey.getInternalKey();
957         if (!expectedKeyType.isInstance(internalKey)) {
958             throw new InvalidAccessException("A parameter or an operation is not supported by the underlying object");
959         }
960         return expectedKeyType.cast(internalKey);
961     }
962 }