Repository: denoland/deno
Pull request: fix(node/crypto): handle empty input in decipher final with auto_pad=false

## Summary

Fixes a panic in `node:crypto` when using `createDecipheriv` with `setAutoPadding(false)` (denoland/deno#31957).

Two cases caused panics in `DecipherContext::final`:
1. **Empty input** — `GenericArray::from_slice` panicked on a 0-length slice when it expected 16 bytes. Now returns `Ok(())` early.
2. **Invalid block length** (not a multiple of 16 bytes) — the assertion inside `generic-array` panicked. Now uses the existing `assert_block_len!` macro to return `DecipherError::InvalidFinalBlockLength` instead.

Affected cipher modes: `Aes128Cbc`, `Aes128Ecb`, `Aes192Ecb`, `Aes256Ecb`, `Aes256Cbc`.

## Changes

- `ext/node/ops/crypto/cipher.rs` — added empty-input guard and `assert_block_len!` validation for all 5 affected `(cipher, auto_pad=false)` match arms
- `tests/unit_node/crypto/crypto_cipher_test.ts` — regression tests for both empty input and invalid block length scenarios

## Test plan

- [x] Unit tests for empty input with `setAutoPadding(false)` across all affected ciphers
- [x] Unit tests for invalid block length (10 bytes) returning `RangeError` instead of panicking
- [ ] `cargo test -p deno_node` passes

Fixes #31957

Changed files:
filename: ext/node_crypto/cipher.rs
patch: @@ -752,10 +752,13 @@ impl Decipher {
         Ok(())
       }
       (Aes128Cbc(mut decryptor), false) => {
-        decryptor.decrypt_block_b2b_mut(
-          GenericArray::from_slice(input),
-          GenericArray::from_mut_slice(output),
-        );
+        if !input.is_empty() {
+          assert_block_len!(input.len(), 16);
+          decryptor.decrypt_block_b2b_mut(
+            GenericArray::from_slice(input),
+            GenericArray::from_mut_slice(output),
+          );
+        }
         Ok(())
       }
       (Aes128Ecb(decryptor), true) => {
@@ -766,10 +769,13 @@ impl Decipher {
         Ok(())
       }
       (Aes128Ecb(mut decryptor), false) => {
-        decryptor.decrypt_block_b2b_mut(
-          GenericArray::from_slice(input),
-          GenericArray::from_mut_slice(output),
-        );
+        if !input.is_empty() {
+          assert_block_len!(input.len(), 16);
+          decryptor.decrypt_block_b2b_mut(
+            GenericArray::from_slice(input),
+            GenericArray::from_mut_slice(output),
+          );
+        }
         Ok(())
       }
       (Aes192Ecb(decryptor), true) => {
@@ -780,10 +786,13 @@ impl Decipher {
         Ok(())
       }
       (Aes192Ecb(mut decryptor), false) => {
-        decryptor.decrypt_block_b2b_mut(
-          GenericArray::from_slice(input),
-          GenericArray::from_mut_slice(output),
-        );
+        if !input.is_empty() {
+          assert_block_len!(input.len(), 16);
+          decryptor.decrypt_block_b2b_mut(
+            GenericArray::from_slice(input),
+            GenericArray::from_mut_slice(output),
+          );
+        }
         Ok(())
       }
       (Aes256Ecb(decryptor), true) => {
@@ -794,10 +803,13 @@ impl Decipher {
         Ok(())
       }
       (Aes256Ecb(mut decryptor), false) => {
-        decryptor.decrypt_block_b2b_mut(
-          GenericArray::from_slice(input),
-          GenericArray::from_mut_slice(output),
-        );
+        if !input.is_empty() {
+          assert_block_len!(input.len(), 16);
+          decryptor.decrypt_block_b2b_mut(
+            GenericArray::from_slice(input),
+            GenericArray::from_mut_slice(output),
+          );
+        }
         Ok(())
       }
       (Aes128Gcm(decipher, auth_tag_length), _) => {
@@ -836,10 +848,13 @@ impl Decipher {
         Ok(())
       }
       (Aes256Cbc(mut decryptor), false) => {
-        decryptor.decrypt_block_b2b_mut(
-          GenericArray::from_slice(input),
-          GenericArray::from_mut_slice(output),
-        );
+        if !input.is_empty() {
+          assert_block_len!(input.len(), 16);
+          decryptor.decrypt_block_b2b_mut(
+            GenericArray::from_slice(input),
+            GenericArray::from_mut_slice(output),
+          );
+        }
         Ok(())
       }
       (Aes256Ctr(mut decryptor), _) => {

filename: tests/unit_node/crypto/crypto_cipher_test.ts
patch: @@ -708,3 +708,173 @@ Deno.test({
     assertEquals(decrypted, secret);
   },
 });
+
+// Regression test for https://github.com/denoland/deno/issues/31957
+Deno.test({
+  name: "createDecipheriv - setAutoPadding(false) with empty final input",
+  fn() {
+    // Test aes-256-ecb (the original issue)
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-256-ecb",
+        Buffer.alloc(32),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      const output = decipher.update(Buffer.alloc(16));
+      assertEquals(output.length, 16);
+      decipher.final();
+    }
+
+    // Test aes-128-ecb
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-128-ecb",
+        Buffer.alloc(16),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      const output = decipher.update(Buffer.alloc(16));
+      assertEquals(output.length, 16);
+      decipher.final();
+    }
+
+    // Test aes-192-ecb
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-192-ecb",
+        Buffer.alloc(24),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      const output = decipher.update(Buffer.alloc(16));
+      assertEquals(output.length, 16);
+      decipher.final();
+    }
+
+    // Test aes-128-cbc
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-128-cbc",
+        Buffer.alloc(16),
+        Buffer.alloc(16),
+      );
+      decipher.setAutoPadding(false);
+      const output = decipher.update(Buffer.alloc(16));
+      assertEquals(output.length, 16);
+      decipher.final();
+    }
+
+    // Test aes-256-cbc
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-256-cbc",
+        Buffer.alloc(32),
+        Buffer.alloc(16),
+      );
+      decipher.setAutoPadding(false);
+      const output = decipher.update(Buffer.alloc(16));
+      assertEquals(output.length, 16);
+      decipher.final();
+    }
+  },
+});
+
+Deno.test({
+  name:
+    "createDecipheriv - setAutoPadding(false) with invalid block length should error",
+  fn() {
+    // Invalid block length (10 bytes instead of 16) should throw an error, not panic
+    // Test all affected cipher modes
+
+    // aes-256-ecb
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-256-ecb",
+        Buffer.alloc(32),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      decipher.update(Buffer.alloc(10));
+      assertThrows(
+        () => {
+          decipher.final();
+        },
+        RangeError,
+        "wrong final block length",
+      );
+    }
+
+    // aes-128-ecb
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-128-ecb",
+        Buffer.alloc(16),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      decipher.update(Buffer.alloc(10));
+      assertThrows(
+        () => {
+          decipher.final();
+        },
+        RangeError,
+        "wrong final block length",
+      );
+    }
+
+    // aes-192-ecb
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-192-ecb",
+        Buffer.alloc(24),
+        "",
+      );
+      decipher.setAutoPadding(false);
+      decipher.update(Buffer.alloc(10));
+      assertThrows(
+        () => {
+          decipher.final();
+        },
+        RangeError,
+        "wrong final block length",
+      );
+    }
+
+    // aes-128-cbc
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-128-cbc",
+        Buffer.alloc(16),
+        Buffer.alloc(16),
+      );
+      decipher.setAutoPadding(false);
+      decipher.update(Buffer.alloc(10));
+      assertThrows(
+        () => {
+          decipher.final();
+        },
+        RangeError,
+        "wrong final block length",
+      );
+    }
+
+    // aes-256-cbc
+    {
+      const decipher = crypto.createDecipheriv(
+        "aes-256-cbc",
+        Buffer.alloc(32),
+        Buffer.alloc(16),
+      );
+      decipher.setAutoPadding(false);
+      decipher.update(Buffer.alloc(10));
+      assertThrows(
+        () => {
+          decipher.final();
+        },
+        RangeError,
+        "wrong final block length",
+      );
+    }
+  },
+});