1
2
3
4
5
6
7
8
9
10
11
12
13
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
70
71
72
73
74
75
76
77
78 @JsxClass
79 public class SubtleCrypto extends HtmlUnitScriptable {
80
81
82
83
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
103
104
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
118
119
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
127
128
129 private static final Set<String> KEY_FORMATS = Set.of("raw", "spki", "pkcs8", "jwk");
130
131
132
133
134
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
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
181
182
183
184
185
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
198
199
200
201
202
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
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
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
252 final Scriptable params = toAlgorithmParams(algorithm, algorithmName);
253
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
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
308 final String hash = ((RsaHashedKeyAlgorithm) key.getKeyAlgorithm()).getHash();
309
310
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
375
376
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
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
417
418
419
420
421
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
434
435
436
437
438
439
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
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
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
485 case "RSA-PSS":
486
487 case "ECDSA": {
488
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
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
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
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
582
583
584
585
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
614
615
616
617
618
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
721
722
723
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
742 if (privateUsages.isEmpty()) {
743 throw new IllegalArgumentException("An invalid or illegal string was specified");
744 }
745
746
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
760
761
762
763 @JsxFunction
764 public NativePromise deriveKey() {
765 return notImplemented();
766 }
767
768
769
770
771
772
773 @JsxFunction
774 public NativePromise deriveBits() {
775 return notImplemented();
776 }
777
778
779
780
781
782
783
784
785
786
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
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
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
869
870
871
872
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
912
913
914
915 @JsxFunction
916 public NativePromise wrapKey() {
917 return notImplemented();
918 }
919
920
921
922
923
924
925 @JsxFunction
926 public NativePromise unwrapKey() {
927 return notImplemented();
928 }
929
930
931
932
933
934
935
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
952
953
954
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
966
967
968
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
979
980
981
982
983
984
985
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
1010
1011
1012
1013
1014 static ByteBuffer asByteBuffer(final Object data) {
1015 if (data instanceof NativeArrayBuffer nativeBuffer) {
1016
1017 if (nativeBuffer.isDetached()) {
1018 return ByteBuffer.allocate(0);
1019 }
1020 return nativeBuffer.getByteBuffer();
1021 }
1022 else if (data instanceof NativeArrayBufferView arrayBufferView) {
1023
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
1041
1042
1043
1044
1045
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
1056
1057
1058
1059
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
1071
1072
1073
1074
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
1086
1087
1088
1089
1090
1091 static byte[] getRequiredBuffer(final Scriptable dictionary, final String member) {
1092 return toByteArray(asByteBuffer(getRequiredMember(dictionary, member)));
1093 }
1094
1095
1096
1097
1098
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
1110
1111
1112
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
1132
1133
1134
1135
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
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
1158
1159
1160
1161
1162
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 }