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