|
16 | 16 |
|
17 | 17 | import java.nio.ByteBuffer; |
18 | 18 | import java.security.GeneralSecurityException; |
| 19 | +import java.security.Key; |
19 | 20 | import java.security.KeyPair; |
20 | 21 | import java.security.KeyPairGenerator; |
21 | 22 | import java.security.MessageDigest; |
| 23 | +import java.security.PrivateKey; |
| 24 | +import java.security.PublicKey; |
| 25 | +import java.security.Signature; |
22 | 26 | import java.security.spec.ECGenParameterSpec; |
| 27 | +import java.security.spec.MGF1ParameterSpec; |
| 28 | +import java.security.spec.PSSParameterSpec; |
23 | 29 | import java.security.spec.RSAKeyGenParameterSpec; |
24 | 30 | import java.util.ArrayList; |
25 | 31 | import java.util.Collections; |
|
30 | 36 | import java.util.Set; |
31 | 37 |
|
32 | 38 | import javax.crypto.KeyGenerator; |
| 39 | +import javax.crypto.Mac; |
33 | 40 | import javax.crypto.SecretKey; |
34 | 41 | import javax.crypto.spec.SecretKeySpec; |
35 | 42 |
|
@@ -87,6 +94,12 @@ public class SubtleCrypto extends HtmlUnitScriptable { |
87 | 94 | new LinkedHashSet<>(List.of("encrypt", "decrypt", "sign", "verify", |
88 | 95 | "deriveKey", "deriveBits", "wrapKey", "unwrapKey"))); |
89 | 96 |
|
| 97 | + private static class InvalidAccessException extends RuntimeException { |
| 98 | + InvalidAccessException(final String message) { |
| 99 | + super(message); |
| 100 | + } |
| 101 | + } |
| 102 | + |
90 | 103 | /** |
91 | 104 | * Creates an instance. |
92 | 105 | */ |
@@ -121,23 +134,157 @@ public NativePromise decrypt() { |
121 | 134 | } |
122 | 135 |
|
123 | 136 | /** |
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 |
127 | 143 | */ |
128 | 144 | @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); |
131 | 147 | } |
132 | 148 |
|
133 | 149 | /** |
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 |
137 | 157 | */ |
138 | 158 | @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; |
141 | 288 | } |
142 | 289 |
|
143 | 290 | /** |
@@ -487,6 +634,33 @@ private static void ensureAlgorithmIsSupported(final String operation, final Str |
487 | 634 | } |
488 | 635 | } |
489 | 636 |
|
| 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 | + |
490 | 664 | /** |
491 | 665 | * Resolves the algorithm name from the given {@code AlgorithmIdentifier}. |
492 | 666 | * @see <a href="https://w3c.github.io/webcrypto/#dfn-AlgorithmIdentifier"> |
@@ -585,4 +759,20 @@ static List<String> resolveKeyUsages(final String algorithm, final Scriptable ke |
585 | 759 |
|
586 | 760 | return sortedKeyUsages; |
587 | 761 | } |
| 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 | + } |
588 | 778 | } |
0 commit comments