Commit b77de58ef56 for nodejs

commit b77de58ef565e7614d164aa75569f886e2143974
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Tue Sep 22 22:32:12 2026 +0200

    crypto: split hybrid keys without species

    Create byte views directly when splitting hybrid KEM keys and seeds,
    so typed-array species cannot alter the component key material.

    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66237
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Aviv Keller <me@aviv.sh>

diff --git a/lib/internal/crypto/kem_hybrids.js b/lib/internal/crypto/kem_hybrids.js
index d95a651b61b..aa8fe27efed 100644
--- a/lib/internal/crypto/kem_hybrids.js
+++ b/lib/internal/crypto/kem_hybrids.js
@@ -8,9 +8,9 @@ const {
   TypedArrayOf,
   TypedArrayPrototypeGetBuffer,
   TypedArrayPrototypeGetByteLength,
+  TypedArrayPrototypeGetByteOffset,
   TypedArrayPrototypeGetLength,
   TypedArrayPrototypeSet,
-  TypedArrayPrototypeSubarray,
   Uint8Array,
 } = primordials;

@@ -111,6 +111,12 @@ const kEncapsulationUsages = ['encapsulateKey', 'encapsulateBits'];
 const kDecapsulationUsages = ['decapsulateKey', 'decapsulateBits'];
 const kUsages = createKeyUsages(kEncapsulationUsages, kDecapsulationUsages);

+function getByteView(data, start, end = TypedArrayPrototypeGetByteLength(data)) {
+  return new Uint8Array(TypedArrayPrototypeGetBuffer(data),
+                        TypedArrayPrototypeGetByteOffset(data) + start,
+                        end - start);
+}
+
 /**
  * Adds lengths that are the concatenation of the PQ KEM component and the
  * traditional group component.
@@ -289,8 +295,8 @@ function validateLength(data, length) {
 function splitAt(data, offset) {
   return {
     __proto__: null,
-    head: TypedArrayPrototypeSubarray(data, 0, offset),
-    tail: TypedArrayPrototypeSubarray(data, offset),
+    head: getByteView(data, 0, offset),
+    tail: getByteView(data, offset),
   };
 }

@@ -353,7 +359,7 @@ function randomScalar(seed, config) {
     offset + groupScalarLength <= TypedArrayPrototypeGetLength(seed);
     offset += groupScalarLength) {
     const scalar = copyBytes(
-      TypedArrayPrototypeSubarray(seed, offset, offset + groupScalarLength));
+      getByteView(seed, offset, offset + groupScalarLength));
     const value = os2ip(scalar);
     if (value !== 0n && value < groupOrder)
       return scalar;
@@ -761,7 +767,7 @@ function combineSharedSecret(
   config) {
   // C2PRICombiner(ss_PQ, ss_T, ct_T, ek_T, Label).
   // https://www.ietf.org/archive/id/draft-irtf-cfrg-hybrid-kems-12.html#section-5.1.3
-  const encapsulationKeyT = TypedArrayPrototypeSubarray(
+  const encapsulationKeyT = getByteView(
     encapsulationKey,
     config.kemPqEncapsulationKeyLength);
   const inputLength =
diff --git a/test/parallel/test-webcrypto-hybrid-kem-species.js b/test/parallel/test-webcrypto-hybrid-kem-species.js
new file mode 100644
index 00000000000..0521e0b7fc4
--- /dev/null
+++ b/test/parallel/test-webcrypto-hybrid-kem-species.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const { subtle } = globalThis.crypto;
+
+(async () => {
+  for (const name of ['MLKEM768-P256', 'MLKEM768-X25519', 'MLKEM1024-P384']) {
+    if (!SubtleCrypto.supports('generateKey', name)) continue;
+    const { privateKey, publicKey } = await subtle.generateKey(
+      name, true, ['encapsulateBits', 'decapsulateBits']);
+    const seed = await subtle.exportKey('raw-seed', privateKey);
+    const publicBytes = await subtle.exportKey('raw-public', publicKey);
+    const descriptor = Object.getOwnPropertyDescriptor(Uint8Array, Symbol.species);
+    try {
+      Object.defineProperty(Uint8Array, Symbol.species, {
+        configurable: true, get: common.mustNotCall(),
+      });
+      const imported = await subtle.importKey('raw-seed', seed, name, true,
+                                              ['decapsulateBits']);
+      assert.deepStrictEqual(await subtle.exportKey('raw-seed', imported), seed);
+      const publicImported = await subtle.importKey('raw-public', publicBytes, name,
+                                                    true, ['encapsulateBits']);
+      assert.deepStrictEqual(await subtle.exportKey('raw-public', publicImported), publicBytes);
+    } finally {
+      if (descriptor) {
+        Object.defineProperty(Uint8Array, Symbol.species, descriptor);
+      } else {
+        delete Uint8Array[Symbol.species];
+      }
+    }
+  }
+})().then(common.mustCall());