Skip to content

Commit 39551e4

Browse files
authored
fix: reject SharedArrayBuffer in WebCrypto and getRandomValues (#1019)
1 parent 5a10d66 commit 39551e4

4 files changed

Lines changed: 364 additions & 15 deletions

File tree

example/src/hooks/useTestsList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import '../tests/subtle/encrypt_decrypt';
4141
import '../tests/subtle/generateKey';
4242
import '../tests/subtle/import_export';
4343
import '../tests/subtle/jwk_rfc7517_tests';
44+
import '../tests/subtle/sharedarraybuffer_rejection';
4445
import '../tests/subtle/sign_verify';
4546
import '../tests/subtle/supports';
4647
import '../tests/subtle/getPublicKey';
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { expect } from 'chai';
2+
import crypto, { subtle, getRandomValues } from 'react-native-quick-crypto';
3+
import type { CryptoKey, HkdfAlgorithm } from 'react-native-quick-crypto';
4+
import { test } from '../util';
5+
6+
// WebCrypto / Web IDL §BufferSource: SharedArrayBuffer-backed inputs must
7+
// be rejected from all subtle.* methods and getRandomValues. Concurrent
8+
// writes during async crypto operations can race with the algorithm,
9+
// corrupting output or leaking intermediate state.
10+
//
11+
// Reference: Node.js commit bee10872588 ("lib: reject SharedArrayBuffer in
12+
// web APIs per spec") — Node throws TypeError, matching the WebIDL
13+
// BufferSource converter and the W3C WebCrypto spec.
14+
15+
const SUITE = 'subtle.sharedarraybuffer-rejection';
16+
17+
// Some hosts (older Hermes builds) don't expose SharedArrayBuffer at all.
18+
// Skip the suite cleanly in that case rather than failing.
19+
const sabAvailable = typeof SharedArrayBuffer !== 'undefined';
20+
21+
function makeSab(byteLength = 16): SharedArrayBuffer {
22+
return new SharedArrayBuffer(byteLength);
23+
}
24+
25+
function makeSabView(byteLength = 16): Uint8Array {
26+
return new Uint8Array(makeSab(byteLength));
27+
}
28+
29+
function expectRejected(err: unknown, label: string) {
30+
expect(err, `${label}: expected an error`).to.be.instanceOf(Error);
31+
// WebIDL BufferSource conversion failure is TypeError per spec / Node.
32+
expect((err as Error).name, `${label}: error name`).to.equal('TypeError');
33+
expect(
34+
(err as Error).message.toLowerCase(),
35+
`${label}: error message mentions SharedArrayBuffer`,
36+
).to.include('sharedarraybuffer');
37+
}
38+
39+
if (sabAvailable) {
40+
// ---- getRandomValues ----------------------------------------------------
41+
42+
test(SUITE, 'getRandomValues rejects SAB-backed Uint8Array', () => {
43+
let caught: unknown;
44+
try {
45+
getRandomValues(makeSabView(8));
46+
} catch (e) {
47+
caught = e;
48+
}
49+
expectRejected(caught, 'getRandomValues');
50+
});
51+
52+
// ---- randomFill / randomFillSync ---------------------------------------
53+
54+
test(SUITE, 'randomFillSync rejects SAB-backed Uint8Array', () => {
55+
let caught: unknown;
56+
try {
57+
crypto.randomFillSync(makeSabView(8));
58+
} catch (e) {
59+
caught = e;
60+
}
61+
expectRejected(caught, 'randomFillSync');
62+
});
63+
64+
test(SUITE, 'randomFillSync rejects raw SharedArrayBuffer', () => {
65+
let caught: unknown;
66+
try {
67+
crypto.randomFillSync(makeSab(8) as unknown as ArrayBuffer);
68+
} catch (e) {
69+
caught = e;
70+
}
71+
expectRejected(caught, 'randomFillSync (raw SAB)');
72+
});
73+
74+
test(SUITE, 'randomFill rejects SAB-backed Uint8Array', () => {
75+
let caught: unknown;
76+
try {
77+
crypto.randomFill(makeSabView(8), () => {
78+
// not reached
79+
});
80+
} catch (e) {
81+
caught = e;
82+
}
83+
expectRejected(caught, 'randomFill');
84+
});
85+
86+
// ---- subtle.digest -----------------------------------------------------
87+
88+
test(SUITE, 'subtle.digest rejects SAB-backed view', async () => {
89+
let caught: unknown;
90+
try {
91+
await subtle.digest('SHA-256', makeSabView(8));
92+
} catch (e) {
93+
caught = e;
94+
}
95+
expectRejected(caught, 'subtle.digest');
96+
});
97+
98+
test(SUITE, 'subtle.digest rejects raw SharedArrayBuffer', async () => {
99+
let caught: unknown;
100+
try {
101+
await subtle.digest('SHA-256', makeSab(8) as unknown as ArrayBuffer);
102+
} catch (e) {
103+
caught = e;
104+
}
105+
expectRejected(caught, 'subtle.digest (raw SAB)');
106+
});
107+
108+
// ---- subtle.encrypt / decrypt ------------------------------------------
109+
110+
test(SUITE, 'subtle.encrypt rejects SAB-backed plaintext', async () => {
111+
const key = await subtle.generateKey(
112+
{ name: 'AES-GCM', length: 256 },
113+
false,
114+
['encrypt', 'decrypt'],
115+
);
116+
const iv = new Uint8Array(12);
117+
let caught: unknown;
118+
try {
119+
await subtle.encrypt(
120+
{ name: 'AES-GCM', iv },
121+
key as CryptoKey,
122+
makeSabView(16),
123+
);
124+
} catch (e) {
125+
caught = e;
126+
}
127+
expectRejected(caught, 'subtle.encrypt');
128+
});
129+
130+
test(SUITE, 'subtle.encrypt rejects SAB-backed iv', async () => {
131+
const key = await subtle.generateKey(
132+
{ name: 'AES-GCM', length: 256 },
133+
false,
134+
['encrypt', 'decrypt'],
135+
);
136+
let caught: unknown;
137+
try {
138+
await subtle.encrypt(
139+
{ name: 'AES-GCM', iv: makeSabView(12) },
140+
key as CryptoKey,
141+
new Uint8Array(16),
142+
);
143+
} catch (e) {
144+
caught = e;
145+
}
146+
expectRejected(caught, 'subtle.encrypt (SAB iv)');
147+
});
148+
149+
test(SUITE, 'subtle.decrypt rejects SAB-backed ciphertext', async () => {
150+
const key = await subtle.generateKey(
151+
{ name: 'AES-GCM', length: 256 },
152+
false,
153+
['encrypt', 'decrypt'],
154+
);
155+
let caught: unknown;
156+
try {
157+
await subtle.decrypt(
158+
{ name: 'AES-GCM', iv: new Uint8Array(12) },
159+
key as CryptoKey,
160+
makeSabView(32),
161+
);
162+
} catch (e) {
163+
caught = e;
164+
}
165+
expectRejected(caught, 'subtle.decrypt');
166+
});
167+
168+
// ---- subtle.sign / verify ---------------------------------------------
169+
170+
test(SUITE, 'subtle.sign rejects SAB-backed data', async () => {
171+
const key = await subtle.generateKey(
172+
{ name: 'HMAC', hash: 'SHA-256' },
173+
false,
174+
['sign', 'verify'],
175+
);
176+
let caught: unknown;
177+
try {
178+
await subtle.sign({ name: 'HMAC' }, key as CryptoKey, makeSabView(16));
179+
} catch (e) {
180+
caught = e;
181+
}
182+
expectRejected(caught, 'subtle.sign');
183+
});
184+
185+
test(SUITE, 'subtle.verify rejects SAB-backed signature', async () => {
186+
const key = await subtle.generateKey(
187+
{ name: 'HMAC', hash: 'SHA-256' },
188+
false,
189+
['sign', 'verify'],
190+
);
191+
let caught: unknown;
192+
try {
193+
await subtle.verify(
194+
{ name: 'HMAC' },
195+
key as CryptoKey,
196+
makeSabView(32),
197+
new Uint8Array(16),
198+
);
199+
} catch (e) {
200+
caught = e;
201+
}
202+
expectRejected(caught, 'subtle.verify (SAB signature)');
203+
});
204+
205+
test(SUITE, 'subtle.verify rejects SAB-backed data', async () => {
206+
const key = await subtle.generateKey(
207+
{ name: 'HMAC', hash: 'SHA-256' },
208+
false,
209+
['sign', 'verify'],
210+
);
211+
let caught: unknown;
212+
try {
213+
await subtle.verify(
214+
{ name: 'HMAC' },
215+
key as CryptoKey,
216+
new Uint8Array(32),
217+
makeSabView(16),
218+
);
219+
} catch (e) {
220+
caught = e;
221+
}
222+
expectRejected(caught, 'subtle.verify (SAB data)');
223+
});
224+
225+
// ---- subtle.importKey --------------------------------------------------
226+
227+
test(SUITE, 'subtle.importKey rejects SAB-backed raw key', async () => {
228+
let caught: unknown;
229+
try {
230+
await subtle.importKey(
231+
'raw',
232+
makeSabView(32),
233+
{ name: 'HMAC', hash: 'SHA-256' },
234+
false,
235+
['sign'],
236+
);
237+
} catch (e) {
238+
caught = e;
239+
}
240+
expectRejected(caught, 'subtle.importKey');
241+
});
242+
243+
// ---- subtle.encrypt AES-GCM additionalData ----------------------------
244+
245+
test(
246+
SUITE,
247+
'subtle.encrypt AES-GCM rejects SAB-backed additionalData',
248+
async () => {
249+
const key = await subtle.generateKey(
250+
{ name: 'AES-GCM', length: 256 },
251+
false,
252+
['encrypt', 'decrypt'],
253+
);
254+
let caught: unknown;
255+
try {
256+
await subtle.encrypt(
257+
{
258+
name: 'AES-GCM',
259+
iv: new Uint8Array(12),
260+
additionalData: makeSabView(16),
261+
},
262+
key as CryptoKey,
263+
new Uint8Array(16),
264+
);
265+
} catch (e) {
266+
caught = e;
267+
}
268+
expectRejected(caught, 'subtle.encrypt (SAB additionalData)');
269+
},
270+
);
271+
272+
// ---- subtle.encrypt AES-CTR counter -----------------------------------
273+
274+
test(SUITE, 'subtle.encrypt AES-CTR rejects SAB-backed counter', async () => {
275+
const key = await subtle.generateKey(
276+
{ name: 'AES-CTR', length: 256 },
277+
false,
278+
['encrypt', 'decrypt'],
279+
);
280+
let caught: unknown;
281+
try {
282+
await subtle.encrypt(
283+
{ name: 'AES-CTR', counter: makeSabView(16), length: 64 },
284+
key as CryptoKey,
285+
new Uint8Array(16),
286+
);
287+
} catch (e) {
288+
caught = e;
289+
}
290+
expectRejected(caught, 'subtle.encrypt (SAB counter)');
291+
});
292+
293+
// ---- subtle.deriveBits (HKDF salt/info) --------------------------------
294+
295+
test(SUITE, 'subtle.deriveBits rejects SAB-backed HKDF salt', async () => {
296+
const baseKey = await subtle.importKey(
297+
'raw',
298+
new Uint8Array(32),
299+
'HKDF',
300+
false,
301+
['deriveBits'],
302+
);
303+
let caught: unknown;
304+
try {
305+
const algorithm = {
306+
name: 'HKDF',
307+
hash: 'SHA-256',
308+
salt: makeSabView(16),
309+
info: new Uint8Array(0),
310+
} satisfies HkdfAlgorithm;
311+
await subtle.deriveBits(
312+
algorithm as Parameters<typeof subtle.deriveBits>[0],
313+
baseKey,
314+
128,
315+
);
316+
} catch (e) {
317+
caught = e;
318+
}
319+
expectRejected(caught, 'subtle.deriveBits (SAB salt)');
320+
});
321+
}

packages/react-native-quick-crypto/src/random.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
abvToArrayBuffer,
55
lazyDOMException,
66
QuotaExceededError,
7+
rejectSharedArrayBuffer,
78
} from './utils';
89
import { NitroModules } from 'react-native-nitro-modules';
910
import type { Random } from './specs/random.nitro';
@@ -320,6 +321,13 @@ function isIntegerTypedArray(value: unknown): boolean {
320321
* @returns The filled data
321322
*/
322323
export function getRandomValues(data: RandomTypedArrays) {
324+
// WebIDL BufferSource conversion (TypeError) must run before the
325+
// WebCrypto-specific integer-type / size checks (TypeMismatchError /
326+
// QuotaExceededError). `randomFillSync` below also rejects SAB via
327+
// `abvToArrayBuffer`, but by then we'd already have thrown the wrong
328+
// error type for a non-integer SAB-view, so the explicit early call is
329+
// load-bearing for spec compliance — not redundant.
330+
rejectSharedArrayBuffer(data);
323331
if (!isIntegerTypedArray(data)) {
324332
throw lazyDOMException(
325333
'The data argument must be an integer-type TypedArray',

0 commit comments

Comments
 (0)