Skip to content

Commit 9548628

Browse files
duonglaiquangrbri
authored andcommitted
SubtleCrypto: implement sign() and verify()
1 parent c38eefc commit 9548628

2 files changed

Lines changed: 360 additions & 10 deletions

File tree

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

Lines changed: 200 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

1717
import java.nio.ByteBuffer;
1818
import java.security.GeneralSecurityException;
19+
import java.security.Key;
1920
import java.security.KeyPair;
2021
import java.security.KeyPairGenerator;
2122
import java.security.MessageDigest;
23+
import java.security.PrivateKey;
24+
import java.security.PublicKey;
25+
import java.security.Signature;
2226
import java.security.spec.ECGenParameterSpec;
27+
import java.security.spec.MGF1ParameterSpec;
28+
import java.security.spec.PSSParameterSpec;
2329
import java.security.spec.RSAKeyGenParameterSpec;
2430
import java.util.ArrayList;
2531
import java.util.Collections;
@@ -30,6 +36,7 @@
3036
import java.util.Set;
3137

3238
import javax.crypto.KeyGenerator;
39+
import javax.crypto.Mac;
3340
import javax.crypto.SecretKey;
3441
import javax.crypto.spec.SecretKeySpec;
3542

@@ -87,6 +94,12 @@ public class SubtleCrypto extends HtmlUnitScriptable {
8794
new LinkedHashSet<>(List.of("encrypt", "decrypt", "sign", "verify",
8895
"deriveKey", "deriveBits", "wrapKey", "unwrapKey")));
8996

97+
private static class InvalidAccessException extends RuntimeException {
98+
InvalidAccessException(final String message) {
99+
super(message);
100+
}
101+
}
102+
90103
/**
91104
* Creates an instance.
92105
*/
@@ -121,23 +134,157 @@ public NativePromise decrypt() {
121134
}
122135

123136
/**
124-
* Not yet implemented.
125-
*
126-
* @return a Promise which will be fulfilled with the signature
137+
* Signs data using the given key.
138+
* @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-sign">SubtleCrypto.sign()</a>
139+
* @param algorithm the algorithm identifier (String or object with name property)
140+
* @param key the CryptoKey to sign with
141+
* @param data the data to sign
142+
* @return a Promise that fulfills with an ArrayBuffer containing the signature
127143
*/
128144
@JsxFunction
129-
public NativePromise sign() {
130-
return notImplemented();
145+
public NativePromise sign(final Object algorithm, final CryptoKey key, final Object data) {
146+
return doSignOrVerify(algorithm, key, null, data, true);
131147
}
132148

133149
/**
134-
* Not yet implemented.
135-
*
136-
* @return a Promise which will be fulfilled with a boolean value indicating whether the signature is valid
150+
* Verifies a signature using the given key.
151+
* @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify">SubtleCrypto.verify()</a>
152+
* @param algorithm the algorithm identifier (String or object with name property)
153+
* @param key the CryptoKey to verify with
154+
* @param signature the signature to verify
155+
* @param data the data that was signed
156+
* @return a Promise that fulfills with a boolean indicating whether the signature is valid
137157
*/
138158
@JsxFunction
139-
public NativePromise verify() {
140-
return notImplemented();
159+
public NativePromise verify(final Object algorithm, final CryptoKey key,
160+
final Object signature, final Object data) {
161+
return doSignOrVerify(algorithm, key, signature, data, false);
162+
}
163+
164+
/**
165+
* Shared sign/verify implementation.
166+
*/
167+
private NativePromise doSignOrVerify(final Object algorithm, final CryptoKey key,
168+
final Object existingSignature, final Object data, final boolean isSigning) {
169+
final Object result;
170+
try {
171+
final String algorithmName = resolveAlgorithmName(algorithm);
172+
final String operation = isSigning ? "sign" : "verify";
173+
ensureAlgorithmIsSupported(operation, algorithmName);
174+
ensureKeyAlgorithmMatches(algorithmName, key);
175+
ensureKeyUsage(key, operation);
176+
177+
final ByteBuffer inputData = asByteBuffer(data);
178+
179+
switch (algorithmName) {
180+
case "HMAC": {
181+
// https://w3c.github.io/webcrypto/#hmac-operations
182+
final SecretKey secretKey = getInternalKey(key, SecretKey.class);
183+
final Mac mac = Mac.getInstance(secretKey.getAlgorithm());
184+
mac.init(secretKey);
185+
mac.update(inputData);
186+
final byte[] macBytes = mac.doFinal();
187+
if (isSigning) {
188+
result = macBytes;
189+
}
190+
else {
191+
result = MessageDigest.isEqual(macBytes,
192+
toByteArray(asByteBuffer(existingSignature)));
193+
}
194+
break;
195+
}
196+
case "RSASSA-PKCS1-v1_5":
197+
// https://w3c.github.io/webcrypto/#rsassa-pkcs1
198+
case "RSA-PSS":
199+
// https://w3c.github.io/webcrypto/#rsa-pss
200+
case "ECDSA": {
201+
// https://w3c.github.io/webcrypto/#ecdsa-operations
202+
final Signature sig = "ECDSA".equals(algorithmName)
203+
? resolveEcdsaSignature(algorithm)
204+
: resolveRsaSignature(algorithmName, algorithm, key);
205+
if (isSigning) {
206+
sig.initSign(getInternalKey(key, PrivateKey.class));
207+
sig.update(inputData);
208+
result = sig.sign();
209+
}
210+
else {
211+
sig.initVerify(getInternalKey(key, PublicKey.class));
212+
sig.update(inputData);
213+
result = sig.verify(toByteArray(asByteBuffer(existingSignature)));
214+
}
215+
break;
216+
}
217+
default:
218+
throw new UnsupportedOperationException(operation + " " + algorithmName);
219+
}
220+
}
221+
catch (final EcmaError e) {
222+
return setupRejectedPromise(() -> e);
223+
}
224+
catch (final InvalidAccessException e) {
225+
return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.INVALID_ACCESS_ERR));
226+
}
227+
catch (final IllegalArgumentException e) {
228+
return setupRejectedPromise(() -> new DOMException(e.getMessage(), DOMException.SYNTAX_ERR));
229+
}
230+
catch (final GeneralSecurityException | UnsupportedOperationException e) {
231+
return setupRejectedPromise(() -> new DOMException("Operation is not supported: " + e.getMessage(),
232+
DOMException.NOT_SUPPORTED_ERR));
233+
}
234+
235+
if (isSigning) {
236+
return setupPromise(() -> createArrayBuffer((byte[]) result));
237+
}
238+
return setupPromise(() -> result);
239+
}
240+
241+
/**
242+
* Resolves the RSA {@link Signature} instance for the given algorithm.
243+
*/
244+
private static Signature resolveRsaSignature(final String algorithmName, final Object algorithmParams,
245+
final CryptoKey key) throws GeneralSecurityException {
246+
final Object hashObj = ScriptableObject.getProperty(key.getAlgorithm(), "hash");
247+
final String hash = resolveAlgorithmName(hashObj);
248+
final String javaHash = hash.replace("-", "");
249+
250+
if ("RSASSA-PKCS1-v1_5".equals(algorithmName)) {
251+
return Signature.getInstance(javaHash + "withRSA");
252+
}
253+
254+
if (!(algorithmParams instanceof Scriptable obj)) {
255+
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
256+
}
257+
final Object saltLengthProp = ScriptableObject.getProperty(obj, "saltLength");
258+
if (!(saltLengthProp instanceof Number num)) {
259+
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
260+
}
261+
final int saltLength = num.intValue();
262+
263+
final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hash);
264+
final PSSParameterSpec pssSpec = new PSSParameterSpec(hash, "MGF1", mgf1Spec, saltLength, 1);
265+
final Signature sig = Signature.getInstance("RSASSA-PSS");
266+
sig.setParameter(pssSpec);
267+
return sig;
268+
}
269+
270+
/**
271+
* Resolves the ECDSA {@link Signature} instance for the given algorithm params.
272+
*/
273+
private static Signature resolveEcdsaSignature(final Object algorithmParams)
274+
throws GeneralSecurityException {
275+
if (!(algorithmParams instanceof Scriptable obj)) {
276+
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
277+
}
278+
final Object hashProp = ScriptableObject.getProperty(obj, "hash");
279+
final String hash = resolveAlgorithmName(hashProp);
280+
final String javaHash = hash.replace("-", "");
281+
return Signature.getInstance(javaHash + "withECDSAinP1363Format");
282+
}
283+
284+
private static byte[] toByteArray(final ByteBuffer buffer) {
285+
final byte[] result = new byte[buffer.remaining()];
286+
buffer.get(result);
287+
return result;
141288
}
142289

143290
/**
@@ -487,6 +634,33 @@ private static void ensureAlgorithmIsSupported(final String operation, final Str
487634
}
488635
}
489636

637+
/**
638+
* Verifies that the operation's algorithm name matches the key's algorithm name.
639+
* @param algorithmName the algorithm name from the operation parameters
640+
* @param key the CryptoKey being used
641+
* @throws InvalidAccessException if the algorithm names don't match
642+
*/
643+
private static void ensureKeyAlgorithmMatches(final String algorithmName, final CryptoKey key) {
644+
final String keyAlgoName = resolveAlgorithmName(key.getAlgorithm());
645+
if (!algorithmName.equals(keyAlgoName)) {
646+
throw new InvalidAccessException(
647+
"A parameter or an operation is not supported by the underlying object");
648+
}
649+
}
650+
651+
/**
652+
* Verifies that the key's usages include the specified usage.
653+
* @param key the CryptoKey being used
654+
* @param usage the required usage (e.g. "encrypt", "sign")
655+
* @throws InvalidAccessException if the key doesn't have the required usage
656+
*/
657+
private static void ensureKeyUsage(final CryptoKey key, final String usage) {
658+
if (!key.getUsagesInternal().contains(usage)) {
659+
throw new InvalidAccessException(
660+
"A parameter or an operation is not supported by the underlying object");
661+
}
662+
}
663+
490664
/**
491665
* Resolves the algorithm name from the given {@code AlgorithmIdentifier}.
492666
* @see <a href="https://w3c.github.io/webcrypto/#dfn-AlgorithmIdentifier">
@@ -585,4 +759,20 @@ static List<String> resolveKeyUsages(final String algorithm, final Scriptable ke
585759

586760
return sortedKeyUsages;
587761
}
762+
763+
/**
764+
* Extracts the internal Java key from a CryptoKey, validating it is the expected type.
765+
* @param <T> the expected key type
766+
* @param cryptoKey the CryptoKey
767+
* @param expectedKeyType the expected class (e.g. SecretKey.class)
768+
* @return the internal key cast to the expected type
769+
* @throws InvalidAccessException if the key is not the expected type
770+
*/
771+
static <T extends Key> T getInternalKey(final CryptoKey cryptoKey, final Class<T> expectedKeyType) {
772+
final Key internalKey = cryptoKey.getInternalKey();
773+
if (!expectedKeyType.isInstance(internalKey)) {
774+
throw new InvalidAccessException("A parameter or an operation is not supported by the underlying object");
775+
}
776+
return expectedKeyType.cast(internalKey);
777+
}
588778
}

0 commit comments

Comments
 (0)