Skip to content

Commit e7d3984

Browse files
duonglaiquangrbri
authored andcommitted
SubtleCrypto: implement digest()
1 parent 0fa1546 commit e7d3984

2 files changed

Lines changed: 170 additions & 5 deletions

File tree

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

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@
1414
*/
1515
package org.htmlunit.javascript.host.crypto;
1616

17+
import java.nio.ByteBuffer;
18+
import java.security.GeneralSecurityException;
19+
import java.security.MessageDigest;
20+
import java.util.Map;
21+
import java.util.Set;
22+
23+
import org.htmlunit.corejs.javascript.EcmaError;
1724
import org.htmlunit.corejs.javascript.NativePromise;
25+
import org.htmlunit.corejs.javascript.Scriptable;
26+
import org.htmlunit.corejs.javascript.ScriptableObject;
27+
import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
28+
import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
1829
import org.htmlunit.javascript.HtmlUnitScriptable;
1930
import org.htmlunit.javascript.JavaScriptEngine;
2031
import org.htmlunit.javascript.configuration.JsxClass;
@@ -28,10 +39,31 @@
2839
* @author Ahmed Ashour
2940
* @author Ronald Brill
3041
* @author Atsushi Nakagawa
42+
* @author Lai Quang Duong
3143
*/
3244
@JsxClass
3345
public class SubtleCrypto extends HtmlUnitScriptable {
3446

47+
/**
48+
* Maps each crypto operation to its supported algorithm names.
49+
* @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
50+
*/
51+
private static final Map<String, Set<String>> OPERATION_TO_SUPPORTED_ALGORITHMS = Map.ofEntries(
52+
Map.entry("encrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
53+
Map.entry("decrypt", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM")),
54+
Map.entry("sign", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
55+
Map.entry("verify", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "ECDSA", "HMAC")),
56+
Map.entry("digest", Set.of("SHA-1", "SHA-256", "SHA-384", "SHA-512")),
57+
Map.entry("generateKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP",
58+
"ECDSA", "ECDH", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC")),
59+
Map.entry("importKey", Set.of("RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP", "ECDSA", "ECDH",
60+
"AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC", "HKDF", "PBKDF2")),
61+
Map.entry("wrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
62+
Map.entry("unwrapKey", Set.of("RSA-OAEP", "AES-CTR", "AES-CBC", "AES-GCM", "AES-KW")),
63+
Map.entry("deriveBits", Set.of("ECDH", "HKDF", "PBKDF2")),
64+
Map.entry("deriveKey", Set.of("ECDH", "HKDF", "PBKDF2"))
65+
);
66+
3567
/**
3668
* Creates an instance.
3769
*/
@@ -86,13 +118,35 @@ public NativePromise verify() {
86118
}
87119

88120
/**
89-
* Not yet implemented.
90-
*
91-
* @return a Promise which will be fulfilled with the digest
121+
* Generates a digest of the given data.
122+
* @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest">SubtleCrypto.digest()</a>
123+
* @param hashAlgorithm a string or an object with a single property name containing the hash algorithm to use
124+
* @param data an object containing the data to be digested
125+
* @return a Promise that fulfills with an ArrayBuffer containing the digest
92126
*/
93127
@JsxFunction
94-
public NativePromise digest() {
95-
return notImplemented();
128+
public NativePromise digest(final Object hashAlgorithm, final Object data) {
129+
final byte[] digest;
130+
try {
131+
final ByteBuffer inputData = asByteBuffer(data);
132+
final String algorithm = resolveAlgorithmName(hashAlgorithm);
133+
ensureAlgorithmIsSupported("digest", algorithm);
134+
135+
final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
136+
messageDigest.update(inputData);
137+
digest = messageDigest.digest();
138+
}
139+
catch (final EcmaError e) {
140+
return setupRejectedPromise(() -> e);
141+
}
142+
catch (final IllegalArgumentException e) {
143+
return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
144+
}
145+
catch (final GeneralSecurityException | UnsupportedOperationException e) {
146+
return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
147+
DOMException.NOT_SUPPORTED_ERR));
148+
}
149+
return setupPromise(() -> createArrayBuffer(digest));
96150
}
97151

98152
/**
@@ -164,4 +218,79 @@ public NativePromise wrapKey() {
164218
public NativePromise unwrapKey() {
165219
return notImplemented();
166220
}
221+
222+
/**
223+
* Checks if the specified crypto operation supports the given algorithm.
224+
* @see <a href="https://w3c.github.io/webcrypto/#algorithm-overview">Algorithm Overview</a>
225+
* @param operation the crypto operation (e.g. "digest", "sign")
226+
* @param algorithm the algorithm name (e.g. "SHA-256", "HMAC")
227+
* @throws UnsupportedOperationException if the operation does not support the algorithm
228+
*/
229+
private static void ensureAlgorithmIsSupported(final String operation, final String algorithm) {
230+
final Set<String> supportedAlgorithms = OPERATION_TO_SUPPORTED_ALGORITHMS.get(operation);
231+
if (supportedAlgorithms == null || !supportedAlgorithms.contains(algorithm)) {
232+
throw new UnsupportedOperationException(operation + " " + algorithm);
233+
}
234+
}
235+
236+
/**
237+
* Resolves the algorithm name from the given {@code AlgorithmIdentifier}.
238+
* @see <a href="https://w3c.github.io/webcrypto/#dfn-AlgorithmIdentifier">
239+
* AlgorithmIdentifier</a>
240+
* @param algorithm the algorithm identifier (String or Scriptable with name property)
241+
* @return the resolved algorithm name
242+
* @throws IllegalArgumentException if the identifier cannot be resolved
243+
*/
244+
static String resolveAlgorithmName(final Object algorithm) {
245+
if (algorithm instanceof String str) {
246+
return str;
247+
}
248+
if (algorithm instanceof Scriptable obj) {
249+
final Object name = ScriptableObject.getProperty(obj, "name");
250+
if (name instanceof String nameStr) {
251+
return nameStr;
252+
}
253+
}
254+
throw new IllegalArgumentException("An invalid or illegal string was specified");
255+
}
256+
257+
/**
258+
* Converts ArrayBuffer or ArrayBufferView to a ByteBuffer.
259+
* @param data the buffer source object
260+
* @return the ByteBuffer wrapping the data
261+
* @throws IllegalArgumentException if data is not a Scriptable or is NOT_FOUND
262+
* @throws EcmaError if data is not an ArrayBuffer or ArrayBufferView
263+
*/
264+
static ByteBuffer asByteBuffer(final Object data) {
265+
if (!(data instanceof Scriptable)) {
266+
throw new IllegalArgumentException("An invalid or illegal string was specified");
267+
}
268+
if (data == Scriptable.NOT_FOUND) {
269+
throw new IllegalArgumentException("An invalid or illegal string was specified");
270+
}
271+
if (data instanceof NativeArrayBuffer nativeBuffer) {
272+
return ByteBuffer.wrap(nativeBuffer.getBuffer());
273+
}
274+
else if (data instanceof NativeArrayBufferView arrayBufferView) {
275+
final NativeArrayBuffer arrayBuffer = arrayBufferView.getBuffer();
276+
return ByteBuffer.wrap(
277+
arrayBuffer.getBuffer(), arrayBufferView.getByteOffset(), arrayBufferView.getByteLength());
278+
}
279+
else {
280+
throw JavaScriptEngine.typeError("Argument could not be converted to any of: ArrayBufferView, ArrayBuffer.");
281+
}
282+
}
283+
284+
/**
285+
* Creates a NativeArrayBuffer with proper scope and prototype from the given bytes.
286+
* @param data the byte array to wrap
287+
* @return the new NativeArrayBuffer
288+
*/
289+
NativeArrayBuffer createArrayBuffer(final byte[] data) {
290+
final NativeArrayBuffer buffer = new NativeArrayBuffer(data.length);
291+
System.arraycopy(data, 0, buffer.getBuffer(), 0, data.length);
292+
buffer.setParentScope(getParentScope());
293+
buffer.setPrototype(ScriptableObject.getClassPrototype(getWindow(), buffer.getClassName()));
294+
return buffer;
295+
}
167296
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* @author Ahmed Ashour
2626
* @author Ronald Brill
2727
* @author Atsushi Nakagawa
28+
* @author Lai Quang Duong
2829
*/
2930
public class SubtleCryptoTest extends WebDriverTestCase {
3031

@@ -144,4 +145,39 @@ public void rsassa() throws Exception {
144145
loadPage2(html);
145146
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
146147
}
148+
149+
/**
150+
* @throws Exception if the test fails
151+
*/
152+
@Test
153+
@Alerts({"SHA-1: 8843d7f92416211de9ebb963ff4ce28125932878",
154+
"SHA-256: c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
155+
"SHA-384: 3c9c30d9f665e74d515c842960d4a451c83a0125fd3de7392d7b37231af10c72"
156+
+ "ea58aedfcdf89a5765bf902af93ecf06",
157+
"SHA-512: 0a50261ebd1a390fed2bf326f2673c145582a6342d523204973d0219337f81616a8069b012587cf"
158+
+ "5635f6925f1b56c360230c19b273500ee013e030601bf2425"})
159+
public void digest() throws Exception {
160+
final String html = DOCTYPE_HTML
161+
+ "<html><head><script>\n"
162+
+ LOG_TITLE_FUNCTION
163+
+ " function test() {\n"
164+
+ " var data = new TextEncoder().encode('foobar');\n"
165+
+ " var algorithms = ['SHA-1', 'SHA-256', {name: 'SHA-384'}, {name: 'SHA-512'}];\n"
166+
+ " var chain = Promise.resolve();\n"
167+
+ " algorithms.forEach(function(algo) {\n"
168+
+ " chain = chain.then(function() {\n"
169+
+ " return window.crypto.subtle.digest(algo, data).then(function(hash) {\n"
170+
+ " var hex = Array.from(new Uint8Array(hash))\n"
171+
+ " .map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');\n"
172+
+ " log((algo.name ? algo.name : algo) + ': ' + hex);\n"
173+
+ " });\n"
174+
+ " });\n"
175+
+ " });\n"
176+
+ " }\n"
177+
+ "</script></head><body onload='test()'>\n"
178+
+ "</body></html>";
179+
180+
loadPage2(html);
181+
verifyTitle2(DEFAULT_WAIT_TIME, getWebDriver(), getExpectedAlerts());
182+
}
147183
}

0 commit comments

Comments
 (0)