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