fix(js): real structuredClone so CryptoKey and buffers survive cloning (#390)
* fix(js): real structuredClone so CryptoKey and buffers survive cloning
structuredClone was JSON.parse(JSON.stringify), which silently drops
ArrayBuffer and TypedArray to {}. A cloned CryptoKey also lost its WeakMap
entry, so crypto.subtle.sign/encrypt/deriveBits threw "Argument is not a
valid CryptoKey" on the clone.
Replace the JSON fallback with a recursive cloner covering ArrayBuffer,
TypedArray, DataView, Map, Set, Date, RegExp, Error, and plain objects,
with cycle and own symbol-keyed property handling. CryptoKey opts in via a
clone hook (keyed by Symbol.toStringTag) that rebuilds the key through
makeKey, so the clone re-enters the WeakMap and stays usable.
Fixes #389.
* fix(js): structuredClone handles DataView and rejects functions and symbols
The TypedArray branch called value.slice(), which DataView lacks, so
cloning any DataView threw TypeError, a new crash in the buffers
category this feature targets. Copy the underlying buffer slice over
the view range and wrap a fresh DataView.
The function/symbol rejection lived after the primitive early-return
and was dead code, so functions and symbols were passed through by
reference instead of throwing DataCloneError (HTML structured clone).
Check first and throw.
Register ArrayBuffer, TypedArray, and DataView in the seen map so
repeated references share one clone, and move the cycle check up.
---------
Co-authored-by: SGavrl <[email protected]>
This commit is contained in:
1 parent
7e2cabf411
commit
a92166897f
2 files changed
+302
-1
No files matched your search
Vendored
+93
-1
@@ -5164,7 +5164,88 @@ globalThis.Crypto = class Crypto {
|
||||
}
|
||||
};
|
||||
globalThis.crypto = globalThis.crypto || new globalThis.Crypto();
|
||||
globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v)));
|
||||
// Real structured clone (not JSON). JSON.parse(JSON.stringify) silently drops
|
||||
// ArrayBuffer/TypedArray (they serialize to {}), so Cloudflare's turnstile
|
||||
// orchestrate loses every byte it tries to round-trip through postMessage and
|
||||
// the challenge never completes (issue #389). Clone buffers, typed arrays,
|
||||
// maps/sets, dates, errors, and plain objects recursively; CryptoKey and other
|
||||
// types that register a clone hook (see crypto.subtle below) are routed there.
|
||||
function _structuredClone(value, seen) {
|
||||
// Functions and symbols are not structured-cloneable (HTML structured clone,
|
||||
// DataCloneError). This must run before the primitive early-return below,
|
||||
// which would otherwise pass them through by reference.
|
||||
if (typeof value === "function" || typeof value === "symbol") {
|
||||
throw new DOMException("Failed to execute 'structuredClone': value could not be cloned.", "DataCloneError");
|
||||
}
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
if (seen.has(value)) return seen.get(value);
|
||||
// Typed arrays: copy the underlying buffer slice. DataView has no .slice(),
|
||||
// so slice its buffer over the view's range and wrap a fresh view.
|
||||
if (ArrayBuffer.isView(value)) {
|
||||
if (value instanceof DataView) {
|
||||
const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
||||
const copy = new DataView(buf);
|
||||
seen.set(value, copy);
|
||||
return copy;
|
||||
}
|
||||
const Ctor = value.constructor;
|
||||
const copy = new Ctor(value.slice());
|
||||
seen.set(value, copy);
|
||||
return copy;
|
||||
}
|
||||
if (value instanceof ArrayBuffer) {
|
||||
const copy = value.slice(0);
|
||||
seen.set(value, copy);
|
||||
return copy;
|
||||
}
|
||||
if (value instanceof SharedArrayBuffer) {
|
||||
return value; // transferable, not copyable
|
||||
}
|
||||
if (value instanceof Date) return new Date(value.getTime());
|
||||
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
||||
if (value instanceof Map) {
|
||||
const m = new Map();
|
||||
seen.set(value, m);
|
||||
for (const [k, v] of value) m.set(_structuredClone(k, seen), _structuredClone(v, seen));
|
||||
return m;
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
const s = new Set();
|
||||
seen.set(value, s);
|
||||
for (const v of value) s.add(_structuredClone(v, seen));
|
||||
return s;
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
const Ctor = value.constructor || Error;
|
||||
const e = new Ctor(value.message);
|
||||
if (value.name) e.name = value.name;
|
||||
if (value.stack) e.stack = value.stack;
|
||||
if (value.cause !== undefined) e.cause = _structuredClone(value.cause, seen);
|
||||
return e;
|
||||
}
|
||||
// Platform objects that carry internal slots opt into cloning via a hook
|
||||
// (CryptoKey re-registers its key material so the clone stays usable by
|
||||
// crypto.subtle). Anything else with a registered hook takes that path.
|
||||
if (typeof value[Symbol.toStringTag] === "string" && globalThis.__obscura_clone_hooks) {
|
||||
const hook = globalThis.__obscura_clone_hooks[value[Symbol.toStringTag]];
|
||||
if (typeof hook === "function") return hook(value, seen);
|
||||
}
|
||||
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
||||
seen.set(value, out);
|
||||
for (const k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
out[k] = _structuredClone(value[k], seen);
|
||||
}
|
||||
}
|
||||
// Symbols are not enumerable via for-in; copy own symbol-keyed properties.
|
||||
const syms = Object.getOwnPropertySymbols(value);
|
||||
for (const s of syms) {
|
||||
const d = Object.getOwnPropertyDescriptor(value, s);
|
||||
if (d && "value" in d) out[s] = _structuredClone(d.value, seen);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
globalThis.structuredClone = globalThis.structuredClone || ((v) => _structuredClone(v, new Map()));
|
||||
globalThis.reportError = globalThis.reportError || ((e) => console.error(e));
|
||||
|
||||
// WHATWG Storage as a legacy platform object: a Proxy routes property access
|
||||
@@ -6958,6 +7039,17 @@ if (!globalThis.crypto.subtle) {
|
||||
}
|
||||
return keyMaterial.get(key);
|
||||
}
|
||||
// A CryptoKey cloned via structuredClone or postMessage is a different
|
||||
// object, so the WeakMap lookup above misses and crypto.subtle throws
|
||||
// "Argument is not a valid CryptoKey". Re-register the (cloned) key's
|
||||
// material so the clone stays usable. The clone hook is dispatched by
|
||||
// _structuredClone via Symbol.toStringTag ("CryptoKey"); registered lazily
|
||||
// because structuredClone is defined before this block (issue #389).
|
||||
globalThis.__obscura_clone_hooks = globalThis.__obscura_clone_hooks || {};
|
||||
globalThis.__obscura_clone_hooks["CryptoKey"] = function (src) {
|
||||
const copy = makeKey(src.type, src.extractable, src.algorithm, src.usages, keyBytes(src));
|
||||
return copy;
|
||||
};
|
||||
|
||||
const toBytes = (data) => {
|
||||
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
||||
|
||||
Reference in new issue
Block a user