Repository: nodejs/node
Pull request: crypto: fix cross-realm `SharedArrayBuffer` validation

https://github.com/nodejs/node/pull/57828 only fixed the code for `ArrayBuffer`, let's handle `SharedArrayBuffer` as well. This also refactors the test to include the nits that didn't make it in the original PR.
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/crypto/webidl.js
patch: @@ -25,9 +25,6 @@ const {
   String,
   TypedArrayPrototypeGetBuffer,
   TypedArrayPrototypeGetSymbolToStringTag,
-  globalThis: {
-    SharedArrayBuffer,
-  },
 } = primordials;
 
 const {
@@ -47,7 +44,7 @@ const {
   validateMaxBufferLength,
   kNamedCurveAliases,
 } = require('internal/crypto/util');
-const { isArrayBuffer } = require('internal/util/types');
+const { isArrayBuffer, isSharedArrayBuffer } = require('internal/util/types');
 
 // https://tc39.es/ecma262/#sec-tonumber
 function toNumber(value, opts = kEmptyObject) {
@@ -195,13 +192,6 @@ converters.object = (V, opts) => {
 
 const isNonSharedArrayBuffer = isArrayBuffer;
 
-function isSharedArrayBuffer(V) {
-  // SharedArrayBuffers can be disabled with --no-harmony-sharedarraybuffer.
-  if (SharedArrayBuffer !== undefined)
-    return ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V);
-  return false;
-}
-
 converters.Uint8Array = (V, opts = kEmptyObject) => {
   if (!ArrayBufferIsView(V) ||
     TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') {

filename: test/parallel/test-crypto-subtle-cross-realm.js
patch: @@ -1,21 +1,18 @@
 'use strict';
-// Flags: --expose-internals
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
 
 const assert = require('assert');
 const { subtle } = globalThis.crypto;
 const vm = require('vm');
-const { isArrayBuffer } = require('internal/util/types');
 
 // Test with same-realm ArrayBuffer
 {
   const samerealmData = new Uint8Array([1, 2, 3, 4]).buffer;
 
   subtle.digest('SHA-256', samerealmData)
     .then(common.mustCall((result) => {
-      assert(isArrayBuffer(result));
       assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
     }));
 }
@@ -35,11 +32,30 @@ const { isArrayBuffer } = require('internal/util/types');
   // This should still work, since we're checking structural type
   subtle.digest('SHA-256', crossrealmBuffer)
     .then(common.mustCall((result) => {
-      assert(isArrayBuffer(result));
       assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
     }));
 }
 
+// Cross-realm SharedArrayBuffer should be handled like any SharedArrayBuffer
+{
+  const context = vm.createContext({});
+  const crossrealmSAB = vm.runInContext('new SharedArrayBuffer(4)', context);
+  assert.notStrictEqual(
+    Object.getPrototypeOf(crossrealmSAB),
+    SharedArrayBuffer.prototype
+  );
+  Promise.allSettled([
+    subtle.digest('SHA-256', new Uint8Array(new SharedArrayBuffer(4))),
+    subtle.digest('SHA-256', new Uint8Array(crossrealmSAB)),
+  ]).then(common.mustCall((r) => {
+    assert.partialDeepStrictEqual(r, [
+      { status: 'rejected' },
+      { status: 'rejected' },
+    ]);
+    assert.strictEqual(r[1].reason.message, r[0].reason.message);
+  }));
+}
+
 // Test with both TypedArray buffer methods
 {
   const context = vm.createContext({});
@@ -48,14 +64,12 @@ const { isArrayBuffer } = require('internal/util/types');
   // Test the .buffer property
   subtle.digest('SHA-256', crossrealmUint8Array.buffer)
     .then(common.mustCall((result) => {
-      assert(isArrayBuffer(result));
       assert.strictEqual(result.byteLength, 32);
     }));
 
   // Test passing the TypedArray directly (should work both before and after the fix)
   subtle.digest('SHA-256', crossrealmUint8Array)
     .then(common.mustCall((result) => {
-      assert(isArrayBuffer(result));
       assert.strictEqual(result.byteLength, 32);
     }));
 }
@@ -76,34 +90,32 @@ const { isArrayBuffer } = require('internal/util/types');
     name: 'AES-GCM',
     length: 256
   }, true, ['encrypt', 'decrypt'])
-    .then(common.mustCall((key) => {
+    .then(async (key) => {
       // Create an initialization vector
       const iv = crypto.getRandomValues(new Uint8Array(12));
 
       // Encrypt using the cross-realm ArrayBuffer
-      return subtle.encrypt(
+      const ciphertext = await subtle.encrypt(
         { name: 'AES-GCM', iv },
         key,
         crossRealmBuffer
-      ).then((ciphertext) => {
+      );
         // Decrypt
-        return subtle.decrypt(
-          { name: 'AES-GCM', iv },
-          key,
-          ciphertext
-        );
-      }).then(common.mustCall((plaintext) => {
+      const plaintext = await subtle.decrypt(
+        { name: 'AES-GCM', iv },
+        key,
+        ciphertext
+      );
         // Verify the decrypted content matches original
-        const decryptedView = new Uint8Array(plaintext);
-        for (let i = 0; i < dataView.length; i++) {
-          assert.strictEqual(
-            decryptedView[i],
-            dataView[i],
-            `Byte at position ${i} doesn't match`
-          );
-        }
-      }));
-    }));
+      const decryptedView = new Uint8Array(plaintext);
+      for (let i = 0; i < dataView.length; i++) {
+        assert.strictEqual(
+          decryptedView[i],
+          dataView[i],
+          `Byte at position ${i} doesn't match`
+        );
+      }
+    }).then(common.mustCall());
 }
 
 // Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer
@@ -122,32 +134,31 @@ const { isArrayBuffer } = require('internal/util/types');
     name: 'AES-GCM',
     length: 256
   }, true, ['encrypt', 'decrypt'])
-    .then(common.mustCall((key) => {
+    .then(async (key) => {
       // Create an initialization vector
       const iv = crypto.getRandomValues(new Uint8Array(12));
 
       // Encrypt using the TypedArray view of cross-realm ArrayBuffer
-      return subtle.encrypt(
+      const ciphertext = await subtle.encrypt(
         { name: 'AES-GCM', iv },
         key,
         dataView
-      ).then((ciphertext) => {
+      );
         // Decrypt
-        return subtle.decrypt(
-          { name: 'AES-GCM', iv },
-          key,
-          ciphertext
+      const plaintext = await subtle.decrypt(
+        { name: 'AES-GCM', iv },
+        key,
+        ciphertext
+      );
+
+      // Verify the decrypted content matches original
+      const decryptedView = new Uint8Array(plaintext);
+      for (let i = 0; i < dataView.length; i++) {
+        assert.strictEqual(
+          decryptedView[i],
+          dataView[i],
+          `Byte at position ${i} doesn't match`
         );
-      }).then(common.mustCall((plaintext) => {
-        // Verify the decrypted content matches original
-        const decryptedView = new Uint8Array(plaintext);
-        for (let i = 0; i < dataView.length; i++) {
-          assert.strictEqual(
-            decryptedView[i],
-            dataView[i],
-            `Byte at position ${i} doesn't match`
-          );
-        }
-      }));
-    }));
+      }
+    }).then(common.mustCall());
 }