Repository: denoland/deno
Pull request: fix(ext/node): improve getCipherInfo and allow repeated Hmac digest()

## Summary

- **getCipherInfo**: Add proper cipher info table with OpenSSL NIDs, `blockSize`, and full metadata. Support NID-based lookup. Import missing `validateInt32`/`validateObject` for argument validation.
- **Hmac.digest()**: Allow repeated `digest()` calls on already-finalized Hmac to return empty (matching Node.js behavior) instead of throwing.
- Enable `test-crypto-getcipherinfo.js` and `test-crypto-hmac.js` in node_compat.

## Test plan

- `cargo test --test node_compat test-crypto-getcipherinfo.js` — passes
- `cargo test --test node_compat test-crypto-hmac.js` — passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/node/polyfills/internal/crypto/hash.ts
patch: @@ -259,7 +259,19 @@ class HmacImpl extends Transform {
   digest(): Buffer;
   digest(encoding: BinaryToTextEncoding): string;
   digest(encoding?: BinaryToTextEncoding): Buffer | string {
-    const result = this.#hash.digest();
+    let result;
+    try {
+      result = this.#hash.digest();
+    } catch (err) {
+      if (err instanceof ERR_CRYPTO_HASH_FINALIZED) {
+        // Already finalized - return empty like Node.js
+        if (encoding && encoding !== "buffer") {
+          return "";
+        }
+        return Buffer.alloc(0);
+      }
+      throw err;
+    }
 
     return new Hash(this.#algorithm).update(this.#opad).update(result)
       .digest(

filename: ext/node/polyfills/internal/crypto/util.ts
patch: @@ -16,6 +16,10 @@ import {
   isArrayBufferView,
 } from "ext:deno_node/internal/util/types.ts";
 import { crypto as constants } from "ext:deno_node/internal_binding/constants.ts";
+import {
+  validateInt32,
+  validateObject,
+} from "ext:deno_node/internal/validators.mjs";
 import {
   kHandle,
   kKeyObject,
@@ -68,20 +72,152 @@ export const ellipticCurves: Array<EllipticCurve> = [
   }, // NIST P-224 EC
 ];
 
+// OpenSSL NID values and cipher metadata.
+// NID values sourced from OpenSSL's include/openssl/obj_mac.h.
+interface CipherInfoResult {
+  name: string;
+  nid: number;
+  blockSize: number;
+  ivLength: number;
+  keyLength: number;
+  mode: string;
+}
+
+const cipherInfoTable: CipherInfoResult[] = [
+  {
+    name: "aes-128-ecb",
+    nid: 418,
+    blockSize: 16,
+    ivLength: 0,
+    keyLength: 16,
+    mode: "ecb",
+  },
+  {
+    name: "aes-128-cbc",
+    nid: 419,
+    blockSize: 16,
+    ivLength: 16,
+    keyLength: 16,
+    mode: "cbc",
+  },
+  {
+    name: "aes-192-ecb",
+    nid: 422,
+    blockSize: 16,
+    ivLength: 0,
+    keyLength: 24,
+    mode: "ecb",
+  },
+  {
+    name: "aes-192-cbc",
+    nid: 423,
+    blockSize: 16,
+    ivLength: 16,
+    keyLength: 24,
+    mode: "cbc",
+  },
+  {
+    name: "aes-256-ecb",
+    nid: 426,
+    blockSize: 16,
+    ivLength: 0,
+    keyLength: 32,
+    mode: "ecb",
+  },
+  {
+    name: "aes-256-cbc",
+    nid: 427,
+    blockSize: 16,
+    ivLength: 16,
+    keyLength: 32,
+    mode: "cbc",
+  },
+  {
+    name: "aes-128-gcm",
+    nid: 895,
+    blockSize: 1,
+    ivLength: 12,
+    keyLength: 16,
+    mode: "gcm",
+  },
+  {
+    name: "aes-192-gcm",
+    nid: 898,
+    blockSize: 1,
+    ivLength: 12,
+    keyLength: 24,
+    mode: "gcm",
+  },
+  {
+    name: "aes-256-gcm",
+    nid: 901,
+    blockSize: 1,
+    ivLength: 12,
+    keyLength: 32,
+    mode: "gcm",
+  },
+  {
+    name: "des-ede3-cbc",
+    nid: 44,
+    blockSize: 8,
+    ivLength: 8,
+    keyLength: 24,
+    mode: "cbc",
+  },
+  {
+    name: "aes-128-ctr",
+    nid: 904,
+    blockSize: 1,
+    ivLength: 16,
+    keyLength: 16,
+    mode: "ctr",
+  },
+  {
+    name: "aes-192-ctr",
+    nid: 905,
+    blockSize: 1,
+    ivLength: 16,
+    keyLength: 24,
+    mode: "ctr",
+  },
+  {
+    name: "aes-256-ctr",
+    nid: 906,
+    blockSize: 1,
+    ivLength: 16,
+    keyLength: 32,
+    mode: "ctr",
+  },
+];
+
+const cipherInfoByName = new Map<string, CipherInfoResult>();
+const cipherInfoByNid = new Map<number, CipherInfoResult>();
+
+for (const info of cipherInfoTable) {
+  cipherInfoByName.set(info.name, info);
+  cipherInfoByNid.set(info.nid, info);
+}
+
+// Aliases
+cipherInfoByName.set("aes128", cipherInfoByName.get("aes-128-cbc")!);
+cipherInfoByName.set("aes192", cipherInfoByName.get("aes-192-cbc")!);
+cipherInfoByName.set("aes256", cipherInfoByName.get("aes-256-cbc")!);
+
+// Ciphers actually supported by the runtime (subset of cipherInfoTable).
 const supportedCiphers = [
   "aes-128-ecb",
+  "aes-128-cbc",
   "aes-192-ecb",
   "aes-256-ecb",
-  "aes-128-cbc",
   "aes-256-cbc",
-  "aes128",
-  "aes256",
   "aes-128-gcm",
   "aes-256-gcm",
   "aes-128-ctr",
   "aes-192-ctr",
   "aes-256-ctr",
   "des-ede3-cbc",
+  "aes128",
+  "aes256",
 ];
 
 export function getCiphers(): string[] {
@@ -147,55 +283,23 @@ export function getCipherInfo(
     }
   }
 
-  // This API is heavily based on OpenSSL's EVP_get_cipherbyname(3) and
-  // EVP_get_cipherbynid(3) functions.
-  //
-  // TODO(@littledivy): write proper cipher info utility in Rust
-  // in future refactors
-  const cipher = supportedCiphers.find((c) => c === nameOrNid);
-  if (cipher === undefined) {
-    return undefined;
-  }
+  const info = typeof nameOrNid === "number"
+    ? cipherInfoByNid.get(nameOrNid)
+    : cipherInfoByName.get(nameOrNid);
 
-  const match = cipher.match(/^(aes)-(\d+)-(\w+)$/);
-  if (match) {
-    const [, name, keyLength, mode] = match;
-    return {
-      name: `${name}-${keyLength}-${mode}`,
-      keyLength: parseInt(keyLength) / 8,
-      mode,
-      ivLength: 16,
-    };
-  }
-
-  if (cipher === "aes128") {
-    return {
-      name: "aes-128-cbc",
-      keyLength: 16,
-      mode: "cbc",
-      ivLength: 16,
-    };
+  if (info === undefined) {
+    return undefined;
   }
 
-  if (cipher === "aes192") {
-    return {
-      name: "aes-192-cbc",
-      keyLength: 24,
-      mode: "cbc",
-      ivLength: 16,
-    };
+  if (keyLength !== undefined && info.keyLength !== keyLength) {
+    return undefined;
   }
 
-  if (cipher === "aes256") {
-    return {
-      name: "aes-256-cbc",
-      keyLength: 32,
-      mode: "cbc",
-      ivLength: 16,
-    };
+  if (ivLength !== undefined && info.ivLength !== ivLength) {
+    return undefined;
   }
 
-  return undefined;
+  return { ...info };
 }
 
 let defaultEncoding = "buffer";

filename: tests/node_compat/config.jsonc
patch: @@ -265,6 +265,8 @@
     "parallel/test-crypto-ecdh-convert-key.js": {},
     "parallel/test-crypto-ecb.js": {},
     "parallel/test-crypto-from-binary.js": {},
+    "parallel/test-crypto-getcipherinfo.js": {},
+    "parallel/test-crypto-hmac.js": {},
     "parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js": {},
     "parallel/test-crypto-keygen-dh-classic.js": {},
     "parallel/test-crypto-keygen-duplicate-deprecated-option.js": {},