Skip to content

Commit 607a044

Browse files
committed
crypto: fix cross-realm check of ArrayBuffer
This patch modifies the isNonSharedArrayBuffer function in the WebIDL implementation for the SubtleCrypto API to properly handle ArrayBuffer instances created in different JavaScript realms. Before this fix, when a TypedArray.buffer from a different realm (e.g., from a VM context or worker thread) was passed to SubtleCrypto.digest(), it would fail with: "TypeError: Failed to execute 'digest' on 'SubtleCrypto': 2nd argument is not instance of ArrayBuffer, Buffer, TypedArray, or DataView." The fix use the isArrayBuffer function from internal/util/types to detect cross-realm ArrayBuffer instances when the prototype chain check fails. This ensures compatibility with TypedArray.buffer across JavaScript realms. See storacha/w3up#1591 for more details.
1 parent 958fd91 commit 607a044

File tree

2 files changed

+155
-4
lines changed

2 files changed

+155
-4
lines changed

lib/internal/crypto/webidl.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
const {
1414
ArrayBufferIsView,
15-
ArrayBufferPrototype,
1615
ArrayPrototypeIncludes,
1716
ArrayPrototypePush,
1817
ArrayPrototypeSort,
@@ -48,6 +47,7 @@ const {
4847
validateMaxBufferLength,
4948
kNamedCurveAliases,
5049
} = require('internal/crypto/util');
50+
const { isArrayBuffer } = require('internal/util/types');
5151

5252
// https://tc39.es/ecma262/#sec-tonumber
5353
function toNumber(value, opts = kEmptyObject) {
@@ -193,9 +193,7 @@ converters.object = (V, opts) => {
193193
return V;
194194
};
195195

196-
function isNonSharedArrayBuffer(V) {
197-
return ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V);
198-
}
196+
const isNonSharedArrayBuffer = isArrayBuffer;
199197

200198
function isSharedArrayBuffer(V) {
201199
// SharedArrayBuffers can be disabled with --no-harmony-sharedarraybuffer.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const { subtle } = globalThis.crypto;
9+
const vm = require('vm');
10+
const { isArrayBuffer } = require('internal/util/types');
11+
12+
// Test with same-realm ArrayBuffer
13+
{
14+
const samerealmData = new Uint8Array([1, 2, 3, 4]).buffer;
15+
16+
subtle.digest('SHA-256', samerealmData)
17+
.then(common.mustCall((result) => {
18+
assert(isArrayBuffer(result));
19+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
20+
}));
21+
}
22+
23+
// Test with cross-realm ArrayBuffer
24+
{
25+
const context = vm.createContext({});
26+
const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context);
27+
const crossrealmBuffer = crossrealmUint8Array.buffer;
28+
29+
// Verify it's truly cross-realm
30+
assert.notStrictEqual(
31+
Object.getPrototypeOf(crossrealmBuffer),
32+
ArrayBuffer.prototype
33+
);
34+
35+
// This should still work, since we're checking structural type
36+
subtle.digest('SHA-256', crossrealmBuffer)
37+
.then(common.mustCall((result) => {
38+
assert(isArrayBuffer(result));
39+
assert.strictEqual(result.byteLength, 32); // SHA-256 is 32 bytes
40+
}));
41+
}
42+
43+
// Test with both TypedArray buffer methods
44+
{
45+
const context = vm.createContext({});
46+
const crossrealmUint8Array = vm.runInContext('new Uint8Array([1, 2, 3, 4])', context);
47+
48+
// Test the .buffer property
49+
subtle.digest('SHA-256', crossrealmUint8Array.buffer)
50+
.then(common.mustCall((result) => {
51+
assert(isArrayBuffer(result));
52+
assert.strictEqual(result.byteLength, 32);
53+
}));
54+
55+
// Test passing the TypedArray directly (should work both before and after the fix)
56+
subtle.digest('SHA-256', crossrealmUint8Array)
57+
.then(common.mustCall((result) => {
58+
assert(isArrayBuffer(result));
59+
assert.strictEqual(result.byteLength, 32);
60+
}));
61+
}
62+
63+
// Test with AES-GCM encryption/decryption using cross-realm ArrayBuffer
64+
{
65+
const context = vm.createContext({});
66+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
67+
68+
// Fill the buffer with some data
69+
const dataView = new Uint8Array(crossRealmBuffer);
70+
for (let i = 0; i < dataView.length; i++) {
71+
dataView[i] = i % 256;
72+
}
73+
74+
// Generate a key
75+
subtle.generateKey({
76+
name: 'AES-GCM',
77+
length: 256
78+
}, true, ['encrypt', 'decrypt'])
79+
.then(common.mustCall((key) => {
80+
// Create an initialization vector
81+
const iv = crypto.getRandomValues(new Uint8Array(12));
82+
83+
// Encrypt using the cross-realm ArrayBuffer
84+
return subtle.encrypt(
85+
{ name: 'AES-GCM', iv },
86+
key,
87+
crossRealmBuffer
88+
).then((ciphertext) => {
89+
// Decrypt
90+
return subtle.decrypt(
91+
{ name: 'AES-GCM', iv },
92+
key,
93+
ciphertext
94+
);
95+
}).then(common.mustCall((plaintext) => {
96+
// Verify the decrypted content matches original
97+
const decryptedView = new Uint8Array(plaintext);
98+
for (let i = 0; i < dataView.length; i++) {
99+
assert.strictEqual(
100+
decryptedView[i],
101+
dataView[i],
102+
`Byte at position ${i} doesn't match`
103+
);
104+
}
105+
}));
106+
}));
107+
}
108+
109+
// Test with AES-GCM using TypedArray view of cross-realm ArrayBuffer
110+
{
111+
const context = vm.createContext({});
112+
const crossRealmBuffer = vm.runInContext('new ArrayBuffer(16)', context);
113+
114+
// Fill the buffer with some data
115+
const dataView = new Uint8Array(crossRealmBuffer);
116+
for (let i = 0; i < dataView.length; i++) {
117+
dataView[i] = i % 256;
118+
}
119+
120+
// Generate a key
121+
subtle.generateKey({
122+
name: 'AES-GCM',
123+
length: 256
124+
}, true, ['encrypt', 'decrypt'])
125+
.then(common.mustCall((key) => {
126+
// Create an initialization vector
127+
const iv = crypto.getRandomValues(new Uint8Array(12));
128+
129+
// Encrypt using the TypedArray view of cross-realm ArrayBuffer
130+
return subtle.encrypt(
131+
{ name: 'AES-GCM', iv },
132+
key,
133+
dataView
134+
).then((ciphertext) => {
135+
// Decrypt
136+
return subtle.decrypt(
137+
{ name: 'AES-GCM', iv },
138+
key,
139+
ciphertext
140+
);
141+
}).then(common.mustCall((plaintext) => {
142+
// Verify the decrypted content matches original
143+
const decryptedView = new Uint8Array(plaintext);
144+
for (let i = 0; i < dataView.length; i++) {
145+
assert.strictEqual(
146+
decryptedView[i],
147+
dataView[i],
148+
`Byte at position ${i} doesn't match`
149+
);
150+
}
151+
}));
152+
}));
153+
}

0 commit comments

Comments
 (0)