Fix infinite recursion in web crypto randomUUID polyfill (#858)
On web (browser/Electron), expo-crypto's randomUUID() and getRandomValues() just forward to globalThis.crypto.* (see expo-crypto/src/ExpoCrypto.web.ts). The previous polyfill installed `g.crypto.randomUUID = () => ExpoCrypto.randomUUID()`, which on web reads back through the same `globalThis.crypto.randomUUID` it just installed, recursing until the stack overflows. The same trap exists for getRandomValues, but it rarely triggers because the native version is almost always present. Capture a bound reference to the native getRandomValues *before* installing the polyfill, and generate UUID v4 in JS from 16 random bytes (RFC 4122 version + variant bits). The fallback path stays on ExpoCrypto.getRandomValues (non-recursive on native), and ExpoCrypto.randomUUID is no longer used.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const expoCryptoMock = vi.hoisted(() => ({
|
||||
getRandomValues: vi.fn(<T extends ArrayBufferView>(array: T): T => array),
|
||||
randomUUID: vi.fn(() => {
|
||||
throw new Error("ExpoCrypto.randomUUID should not be used for the web fallback");
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("expo-crypto", () => expoCryptoMock);
|
||||
|
||||
describe("polyfillCrypto", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
expoCryptoMock.getRandomValues.mockClear();
|
||||
expoCryptoMock.randomUUID.mockClear();
|
||||
});
|
||||
|
||||
it("generates randomUUID from getRandomValues when Web Crypto randomUUID is unavailable", async () => {
|
||||
const sourceBytes = Uint8Array.from([
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
|
||||
0xff,
|
||||
]);
|
||||
const getRandomValues = vi.fn(<T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array && ArrayBuffer.isView(array)) {
|
||||
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).set(sourceBytes);
|
||||
}
|
||||
return array;
|
||||
});
|
||||
vi.stubGlobal("crypto", { getRandomValues });
|
||||
|
||||
const { polyfillCrypto } = await import("./crypto");
|
||||
polyfillCrypto();
|
||||
|
||||
expect(globalThis.crypto.randomUUID()).toBe("00112233-4455-4677-8899-aabbccddeeff");
|
||||
expect(getRandomValues).toHaveBeenCalledTimes(1);
|
||||
expect(expoCryptoMock.randomUUID).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,26 @@ interface MutableGlobal {
|
||||
crypto?: Crypto;
|
||||
}
|
||||
|
||||
type RandomUUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||
type FillRandomValues = <T extends ArrayBufferView | null>(array: T) => T;
|
||||
|
||||
function createUuidV4(fillRandomValues: FillRandomValues): RandomUUID {
|
||||
const bytes = fillRandomValues(new Uint8Array(16));
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
||||
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex
|
||||
.slice(6, 8)
|
||||
.join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}` as RandomUUID;
|
||||
}
|
||||
|
||||
export function polyfillCrypto(): void {
|
||||
const g = globalThis as unknown as MutableGlobal;
|
||||
const nativeGetRandomValues =
|
||||
typeof g.crypto?.getRandomValues === "function"
|
||||
? g.crypto.getRandomValues.bind(g.crypto)
|
||||
: null;
|
||||
|
||||
// Ensure TextEncoder/TextDecoder exist for shared E2EE code (tweetnacl + relay transport).
|
||||
// Hermes may not provide them in all configurations.
|
||||
@@ -47,17 +65,21 @@ export function polyfillCrypto(): void {
|
||||
g.crypto = {} as Crypto;
|
||||
}
|
||||
|
||||
const fillRandomValues: FillRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array === null) return array;
|
||||
if (nativeGetRandomValues) {
|
||||
return nativeGetRandomValues(array as unknown as ArrayBufferView<ArrayBuffer>) as T;
|
||||
}
|
||||
return ExpoCrypto.getRandomValues(
|
||||
array as unknown as Parameters<typeof ExpoCrypto.getRandomValues>[0],
|
||||
) as unknown as T;
|
||||
};
|
||||
|
||||
if (typeof g.crypto.randomUUID !== "function") {
|
||||
g.crypto.randomUUID = () =>
|
||||
ExpoCrypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`;
|
||||
g.crypto.randomUUID = () => createUuidV4(fillRandomValues);
|
||||
}
|
||||
|
||||
if (typeof g.crypto.getRandomValues !== "function") {
|
||||
g.crypto.getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array === null) return array;
|
||||
return ExpoCrypto.getRandomValues(
|
||||
array as unknown as Parameters<typeof ExpoCrypto.getRandomValues>[0],
|
||||
) as unknown as T;
|
||||
};
|
||||
g.crypto.getRandomValues = fillRandomValues;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user