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