2323import java .security .PrivateKey ;
2424import java .security .PublicKey ;
2525import java .security .Signature ;
26+ import java .security .spec .AlgorithmParameterSpec ;
2627import java .security .spec .ECGenParameterSpec ;
2728import java .security .spec .MGF1ParameterSpec ;
2829import java .security .spec .PSSParameterSpec ;
3536import java .util .Map ;
3637import java .util .Set ;
3738
39+ import javax .crypto .Cipher ;
3840import javax .crypto .KeyGenerator ;
3941import javax .crypto .Mac ;
4042import 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 ;
4147import javax .crypto .spec .SecretKeySpec ;
4248
4349import org .htmlunit .corejs .javascript .EcmaError ;
@@ -94,6 +100,11 @@ public class SubtleCrypto extends HtmlUnitScriptable {
94100 new LinkedHashSet <>(List .of ("encrypt" , "decrypt" , "sign" , "verify" ,
95101 "deriveKey" , "deriveBits" , "wrapKey" , "unwrapKey" )));
96102
103+ /**
104+ * @see <a href="https://w3c.github.io/webcrypto/#aes-gcm-operations">AES-GCM encrypt, step 6</a>
105+ */
106+ private static final Set <Integer > VALID_AES_GCM_TAG_LENGTHS = Set .of (32 , 64 , 96 , 104 , 112 , 120 , 128 );
107+
97108 private static class InvalidAccessException extends RuntimeException {
98109 InvalidAccessException (final String message ) {
99110 super (message );
@@ -114,23 +125,183 @@ private NativePromise notImplemented() {
114125 }
115126
116127 /**
117- * Not yet implemented.
118- *
119- * @return a Promise which will be fulfilled with the encrypted data (also known as "ciphertext")
128+ * Encrypts data using the given key and algorithm.
129+ * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-encrypt">SubtleCrypto.encrypt()</a>
130+ * @param algorithm the algorithm identifier with parameters
131+ * @param key the CryptoKey to encrypt with
132+ * @param data the data to encrypt
133+ * @return a Promise that fulfills with an ArrayBuffer containing the ciphertext
120134 */
121135 @ JsxFunction
122- public NativePromise encrypt () {
123- return notImplemented ( );
136+ public NativePromise encrypt (final Object algorithm , final CryptoKey key , final Object data ) {
137+ return doCipher ( algorithm , key , data , Cipher . ENCRYPT_MODE );
124138 }
125139
126140 /**
127- * Not yet implemented.
128- *
129- * @return a Promise which will be fulfilled with the decrypted data (also known as "plaintext")
141+ * Decrypts data using the given key and algorithm.
142+ * @see <a href="https://w3c.github.io/webcrypto/#SubtleCrypto-method-decrypt">SubtleCrypto.decrypt()</a>
143+ * @param algorithm the algorithm identifier with parameters
144+ * @param key the CryptoKey to decrypt with
145+ * @param data the data to decrypt
146+ * @return a Promise that fulfills with an ArrayBuffer containing the plaintext
130147 */
131148 @ JsxFunction
132- public NativePromise decrypt () {
133- return notImplemented ();
149+ public NativePromise decrypt (final Object algorithm , final CryptoKey key , final Object data ) {
150+ return doCipher (algorithm , key , data , Cipher .DECRYPT_MODE );
151+ }
152+
153+ /**
154+ * Shared encrypt/decrypt implementation.
155+ */
156+ private NativePromise doCipher (final Object algorithm , final CryptoKey key ,
157+ final Object data , final int cipherMode ) {
158+ final String operation = switch (cipherMode ) {
159+ case Cipher .ENCRYPT_MODE -> "encrypt" ;
160+ case Cipher .DECRYPT_MODE -> "decrypt" ;
161+ default -> throw new IllegalArgumentException ("Invalid cipher mode: " + cipherMode );
162+ };
163+
164+ final byte [] result ;
165+ try {
166+ final String algorithmName = resolveAlgorithmName (algorithm );
167+ ensureAlgorithmIsSupported (operation , algorithmName );
168+ ensureKeyAlgorithmMatches (algorithmName , key );
169+ ensureKeyUsage (key , operation );
170+
171+ final ByteBuffer inputData = asByteBuffer (data );
172+
173+ // encrypt/decrypt requires algorithm parameters as an object (iv, counter, etc.)
174+ if (!(algorithm instanceof Scriptable algorithmObj )) {
175+ throw new IllegalArgumentException ("An invalid or illegal string was specified" );
176+ }
177+
178+ switch (algorithmName ) {
179+ case "AES-CBC" : {
180+ // https://w3c.github.io/webcrypto/#aes-cbc-operations
181+ final byte [] iv = extractBuffer (algorithmObj , "iv" );
182+ if (iv == null || iv .length != 16 ) {
183+ throw new IllegalArgumentException (
184+ "Data provided to an operation does not meet requirements" );
185+ }
186+ final SecretKey secretKey = getInternalKey (key , SecretKey .class );
187+ final Cipher cipher = Cipher .getInstance ("AES/CBC/PKCS5Padding" );
188+ cipher .init (cipherMode , secretKey , new IvParameterSpec (iv ));
189+ result = cipher .doFinal (toByteArray (inputData ));
190+ break ;
191+ }
192+ case "AES-GCM" : {
193+ // https://w3c.github.io/webcrypto/#aes-gcm-operations
194+ final byte [] iv = extractBuffer (algorithmObj , "iv" );
195+ if (iv == null || iv .length == 0 ) {
196+ throw new IllegalArgumentException (
197+ "Data provided to an operation does not meet requirements" );
198+ }
199+
200+ final int tagLength ;
201+ final Object tagLengthProp = ScriptableObject .getProperty (algorithmObj , "tagLength" );
202+ if (tagLengthProp instanceof Number num ) {
203+ tagLength = num .intValue ();
204+ if (!VALID_AES_GCM_TAG_LENGTHS .contains (tagLength )) {
205+ throw new IllegalArgumentException (
206+ "Data provided to an operation does not meet requirements" );
207+ }
208+ }
209+ else {
210+ tagLength = 128 ;
211+ }
212+
213+ final SecretKey secretKey = getInternalKey (key , SecretKey .class );
214+ final Cipher cipher = Cipher .getInstance ("AES/GCM/NoPadding" );
215+ cipher .init (cipherMode , secretKey , new GCMParameterSpec (tagLength , iv ));
216+
217+ final Object aadProp = ScriptableObject .getProperty (algorithmObj , "additionalData" );
218+ if (aadProp instanceof Scriptable ) {
219+ final ByteBuffer aad = asByteBuffer (aadProp );
220+ cipher .updateAAD (toByteArray (aad ));
221+ }
222+
223+ result = cipher .doFinal (toByteArray (inputData ));
224+ break ;
225+ }
226+ case "AES-CTR" : {
227+ // https://w3c.github.io/webcrypto/#aes-ctr-operations
228+ final byte [] counter = extractBuffer (algorithmObj , "counter" );
229+ if (counter == null || counter .length != 16 ) {
230+ throw new IllegalArgumentException (
231+ "Data provided to an operation does not meet requirements" );
232+ }
233+
234+ final Object lengthProp = ScriptableObject .getProperty (algorithmObj , "length" );
235+ if (!(lengthProp instanceof Number numLength )) {
236+ throw new IllegalArgumentException (
237+ "Data provided to an operation does not meet requirements" );
238+ }
239+ final int counterLength = numLength .intValue ();
240+ if (counterLength < 1 || counterLength > 128 ) {
241+ throw new IllegalArgumentException (
242+ "Data provided to an operation does not meet requirements" );
243+ }
244+
245+ final SecretKey secretKey = getInternalKey (key , SecretKey .class );
246+ // Java always increments the full 128-bit counter, ignoring the 'length' partitioning.
247+ // This only becomes an issue when data exceeds 2^length AES blocks (16 bytes each),
248+ // but in real-world usage (length >= 64) it's pretty much unreachable.
249+ final Cipher cipher = Cipher .getInstance ("AES/CTR/NoPadding" );
250+ cipher .init (cipherMode , secretKey , new IvParameterSpec (counter ));
251+ result = cipher .doFinal (toByteArray (inputData ));
252+ break ;
253+ }
254+ case "RSA-OAEP" : {
255+ // https://w3c.github.io/webcrypto/#rsa-oaep-operations
256+ final Scriptable keyAlgorithm = key .getAlgorithm ();
257+ final Object hashObj = ScriptableObject .getProperty (keyAlgorithm , "hash" );
258+ final String hash = resolveAlgorithmName (hashObj );
259+
260+ final byte [] label ;
261+ final Object labelProp = ScriptableObject .getProperty (algorithmObj , "label" );
262+ if (labelProp instanceof Scriptable ) {
263+ final ByteBuffer labelBuf = asByteBuffer (labelProp );
264+ label = toByteArray (labelBuf );
265+ }
266+ else {
267+ label = new byte [0 ];
268+ }
269+
270+ final MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec (hash );
271+ final AlgorithmParameterSpec oaepSpec = new OAEPParameterSpec (
272+ hash , "MGF1" , mgf1Spec , new PSource .PSpecified (label ));
273+
274+ final Key internalKey ;
275+ if (cipherMode == Cipher .ENCRYPT_MODE ) {
276+ internalKey = getInternalKey (key , PublicKey .class );
277+ }
278+ else {
279+ internalKey = getInternalKey (key , PrivateKey .class );
280+ }
281+
282+ final Cipher cipher = Cipher .getInstance ("RSA/ECB/OAEPPadding" );
283+ cipher .init (cipherMode , internalKey , oaepSpec );
284+ result = cipher .doFinal (toByteArray (inputData ));
285+ break ;
286+ }
287+ default :
288+ throw new UnsupportedOperationException (operation + " " + algorithmName );
289+ }
290+ }
291+ catch (final EcmaError e ) {
292+ return setupRejectedPromise (() -> e );
293+ }
294+ catch (final InvalidAccessException e ) {
295+ return setupRejectedPromise (() -> new DOMException (e .getMessage (), DOMException .INVALID_ACCESS_ERR ));
296+ }
297+ catch (final IllegalArgumentException e ) {
298+ return setupRejectedPromise (() -> new DOMException (e .getMessage (), DOMException .SYNTAX_ERR ));
299+ }
300+ catch (final GeneralSecurityException | UnsupportedOperationException e ) {
301+ return setupRejectedPromise (() -> new DOMException ("Operation is not supported: " + e .getMessage (),
302+ DOMException .NOT_SUPPORTED_ERR ));
303+ }
304+ return setupPromise (() -> createArrayBuffer (result ));
134305 }
135306
136307 /**
@@ -709,6 +880,21 @@ else if (data instanceof NativeArrayBufferView arrayBufferView) {
709880 }
710881 }
711882
883+ /**
884+ * Reads a property from a JS object and converts it to a byte array.
885+ * @param obj the JS object containing the property
886+ * @param property the property name (e.g. "iv", "counter", "label")
887+ * @return the byte array, or {@code null} if the property is absent or not convertible
888+ */
889+ private static byte [] extractBuffer (final Scriptable obj , final String property ) {
890+ final Object prop = ScriptableObject .getProperty (obj , property );
891+ if (prop instanceof Scriptable ) {
892+ final ByteBuffer buf = asByteBuffer (prop );
893+ return toByteArray (buf );
894+ }
895+ return null ;
896+ }
897+
712898 /**
713899 * Creates a NativeArrayBuffer with proper scope and prototype from the given bytes.
714900 * @param data the byte array to wrap
0 commit comments