Skip to content

Commit c38eefc

Browse files
duonglaiquangrbri
authored andcommitted
SubtleCrypto: implement importKey() and exportKey()
1 parent 47d58c0 commit c38eefc

2 files changed

Lines changed: 217 additions & 10 deletions

File tree

src/main/java/org/htmlunit/javascript/host/crypto/SubtleCrypto.java

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import javax.crypto.KeyGenerator;
3333
import javax.crypto.SecretKey;
34+
import javax.crypto.spec.SecretKeySpec;
3435

3536
import org.htmlunit.corejs.javascript.EcmaError;
3637
import org.htmlunit.corejs.javascript.NativeObject;
@@ -329,23 +330,127 @@ public NativePromise deriveBits() {
329330
}
330331

331332
/**
332-
* Not yet implemented.
333-
*
334-
* @return a CryptoKey object that you can use in the Web Crypto API
333+
* Imports a key from external, portable key material.
334+
* @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-importKey">SubtleCrypto.importKey()</a>
335+
* @param format the data format ("raw", "pkcs8", "spki", "jwk")
336+
* @param keyData the key material (BufferSource for raw/pkcs8/spki, JsonWebKey for jwk)
337+
* @param keyImportParams algorithm-specific import parameters
338+
* @param isExtractable whether the key can be exported
339+
* @param keyUsages permitted operations for this key
340+
* @return a Promise that fulfills with the imported CryptoKey
335341
*/
336342
@JsxFunction
337-
public NativePromise importKey() {
338-
return notImplemented();
343+
public NativePromise importKey(final String format, final Scriptable keyData,
344+
final Scriptable keyImportParams, final boolean isExtractable, final Scriptable keyUsages) {
345+
final CryptoKey key;
346+
try {
347+
final String algorithm = resolveAlgorithmName(keyImportParams);
348+
ensureAlgorithmIsSupported("importKey", algorithm);
349+
350+
switch (format) {
351+
case "raw":
352+
key = importRawKey(algorithm, keyData, keyImportParams, isExtractable, keyUsages);
353+
break;
354+
case "pkcs8":
355+
case "spki":
356+
case "jwk":
357+
return notImplemented();
358+
default:
359+
throw new IllegalArgumentException("An invalid or illegal string was specified");
360+
}
361+
}
362+
catch (final EcmaError e) {
363+
return setupRejectedPromise(() -> e);
364+
}
365+
catch (final IllegalArgumentException e) {
366+
return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
367+
}
368+
catch (final UnsupportedOperationException e) {
369+
return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
370+
DOMException.NOT_SUPPORTED_ERR));
371+
}
372+
return setupPromise(() -> key);
373+
}
374+
375+
private CryptoKey importRawKey(final String algorithm, final Scriptable keyData,
376+
final Scriptable keyImportParams, final boolean isExtractable, final Scriptable keyUsages) {
377+
final ByteBuffer byteBuffer = asByteBuffer(keyData);
378+
final byte[] rawBytes = new byte[byteBuffer.remaining()];
379+
byteBuffer.get(rawBytes);
380+
final int bitLength = rawBytes.length * 8;
381+
if (bitLength == 0) {
382+
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
383+
}
384+
385+
final List<String> usages = resolveKeyUsages(algorithm, keyUsages);
386+
if (usages.isEmpty()) {
387+
throw new IllegalArgumentException("An invalid or illegal string was specified");
388+
}
389+
390+
if ("HMAC".equals(algorithm)) {
391+
final HmacKeyAlgorithm params = HmacKeyAlgorithm.from(keyImportParams, bitLength);
392+
final int length = params.getLength();
393+
if (length > bitLength || length <= bitLength - 8) {
394+
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
395+
}
396+
397+
final Scriptable scriptableAlgorithm = params.toScriptableObject(keyImportParams.getParentScope());
398+
final SecretKey internalKey = new SecretKeySpec(rawBytes, params.getJavaName());
399+
return CryptoKey.create(getParentScope(), internalKey, isExtractable, scriptableAlgorithm, usages);
400+
}
401+
402+
if (AesKeyAlgorithm.isSupported(algorithm)) {
403+
final AesKeyAlgorithm aesAlgo = new AesKeyAlgorithm(algorithm, bitLength);
404+
final Scriptable scriptableAlgorithm = aesAlgo.toScriptableObject(keyImportParams.getParentScope());
405+
final SecretKey internalKey = new SecretKeySpec(rawBytes, "AES");
406+
return CryptoKey.create(getParentScope(), internalKey, isExtractable, scriptableAlgorithm, usages);
407+
}
408+
409+
throw new UnsupportedOperationException("importKey raw " + algorithm);
339410
}
340411

341412
/**
342-
* Not yet implemented.
343-
*
344-
* @return the key in an external, portable format
413+
* Exports a key in the specified format.
414+
* @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-exportKey">SubtleCrypto.exportKey()</a>
415+
* @param format the data format ("raw", "pkcs8", "spki", "jwk")
416+
* @param key the CryptoKey to export
417+
* @return a Promise that fulfills with the key data
345418
*/
346419
@JsxFunction
347-
public NativePromise exportKey() {
348-
return notImplemented();
420+
public NativePromise exportKey(final String format, final CryptoKey key) {
421+
final byte[] result;
422+
try {
423+
if (!key.getExtractable()) {
424+
return setupRejectedPromise(() -> new DOMException(
425+
"A parameter or an operation is not supported by the underlying object",
426+
DOMException.INVALID_ACCESS_ERR));
427+
}
428+
429+
switch (format) {
430+
case "raw": {
431+
if (!(key.getInternalKey() instanceof SecretKey secretKey)) {
432+
throw new IllegalArgumentException(
433+
"Data provided to an operation does not meet requirements");
434+
}
435+
result = secretKey.getEncoded();
436+
break;
437+
}
438+
case "pkcs8":
439+
case "spki":
440+
case "jwk":
441+
return notImplemented();
442+
default:
443+
throw new IllegalArgumentException("An invalid or illegal string was specified");
444+
}
445+
}
446+
catch (final IllegalArgumentException e) {
447+
return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
448+
}
449+
catch (final UnsupportedOperationException e) {
450+
return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
451+
DOMException.NOT_SUPPORTED_ERR));
452+
}
453+
return setupPromise(() -> createArrayBuffer(result));
349454
}
350455

351456
/**

src/test/java/org/htmlunit/javascript/host/crypto/SubtleCryptoTest.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,71 @@ public void digest() throws Exception {
177177
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
178178
}
179179

180+
/**
181+
* @throws Exception if the test fails
182+
*/
183+
@Test
184+
@Alerts({"secret", "true", "HMAC", "SHA-1", "512", "sign,verify"})
185+
public void importKeyHmac() throws Exception {
186+
final String html = DOCTYPE_HTML
187+
+ "<html><head><script>\n"
188+
+ LOG_TITLE_FUNCTION
189+
+ " function test() {\n"
190+
+ " var rawKey = new Uint8Array([154,96,73,78,144,193,22,2,31,117,82,100,53,153,70,89,"
191+
+ "47,64,159,6,172,145,82,124,25,206,252,42,160,14,136,161,78,165,11,207,226,149,165,112,"
192+
+ "172,10,127,12,252,112,105,222,227,36,1,7,227,17,178,234,9,44,20,40,127,188,114,56]);\n"
193+
+ " window.crypto.subtle.importKey(\n"
194+
+ " 'raw', rawKey,\n"
195+
+ " { name: 'HMAC', hash: { name: 'SHA-1' } },\n"
196+
+ " true, ['verify', 'sign', 'verify']\n"
197+
+ " ).then(function(key) {\n"
198+
+ " log(key.type);\n"
199+
+ " log(key.extractable);\n"
200+
+ " log(key.algorithm.name);\n"
201+
+ " log(key.algorithm.hash.name);\n"
202+
+ " log(key.algorithm.length);\n"
203+
+ " log(key.usages.join(','));\n"
204+
+ " });\n"
205+
+ " }\n"
206+
+ "</script></head><body onload='test()'>\n"
207+
+ "</body></html>";
208+
209+
loadPage2(html);
210+
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
211+
}
212+
213+
/**
214+
* @throws Exception if the test fails
215+
*/
216+
@Test
217+
@Alerts({"secret", "false", "AES-GCM", "256", "encrypt,decrypt"})
218+
public void importKeyAes() throws Exception {
219+
final String html = DOCTYPE_HTML
220+
+ "<html><head><script>\n"
221+
+ LOG_TITLE_FUNCTION
222+
+ " function test() {\n"
223+
+ " var rawKey = new Uint8Array(["
224+
+ "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,"
225+
+ "17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]);\n"
226+
+ " window.crypto.subtle.importKey(\n"
227+
+ " 'raw', rawKey,\n"
228+
+ " { name: 'AES-GCM' },\n"
229+
+ " false, ['encrypt', 'decrypt']\n"
230+
+ " ).then(function(key) {\n"
231+
+ " log(key.type);\n"
232+
+ " log(key.extractable);\n"
233+
+ " log(key.algorithm.name);\n"
234+
+ " log(key.algorithm.length);\n"
235+
+ " log(key.usages.join(','));\n"
236+
+ " });\n"
237+
+ " }\n"
238+
+ "</script></head><body onload='test()'>\n"
239+
+ "</body></html>";
240+
241+
loadPage2(html);
242+
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
243+
}
244+
180245
/**
181246
* @throws Exception if the test fails
182247
*/
@@ -267,4 +332,41 @@ public void generateKeyEc() throws Exception {
267332
loadPage2(html);
268333
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
269334
}
335+
336+
/**
337+
* @throws Exception if the test fails
338+
*/
339+
@Test
340+
@Alerts({"1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16", "rejected"})
341+
public void exportKeyRaw() throws Exception {
342+
final String html = DOCTYPE_HTML
343+
+ "<html><head><script>\n"
344+
+ LOG_TITLE_FUNCTION
345+
+ " function test() {\n"
346+
+ " var rawKey = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);\n"
347+
+ " window.crypto.subtle.importKey(\n"
348+
+ " 'raw', rawKey,\n"
349+
+ " { name: 'AES-GCM' },\n"
350+
+ " true, ['encrypt', 'decrypt']\n"
351+
+ " ).then(function(key) {\n"
352+
+ " return window.crypto.subtle.exportKey('raw', key);\n"
353+
+ " }).then(function(exported) {\n"
354+
+ " log(new Uint8Array(exported).toString());\n"
355+
+ " });\n"
356+
+ " window.crypto.subtle.importKey(\n"
357+
+ " 'raw', rawKey,\n"
358+
+ " { name: 'AES-GCM' },\n"
359+
+ " false, ['encrypt', 'decrypt']\n"
360+
+ " ).then(function(key) {\n"
361+
+ " return window.crypto.subtle.exportKey('raw', key);\n"
362+
+ " }).catch(function(e) {\n"
363+
+ " log('rejected');\n"
364+
+ " });\n"
365+
+ " }\n"
366+
+ "</script></head><body onload='test()'>\n"
367+
+ "</body></html>";
368+
369+
loadPage2(html);
370+
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
371+
}
270372
}

0 commit comments

Comments
 (0)