Repository navigation
Polyfill crypto.randomUUID() and crypto.subtle.digest() for non-secure contexts - #392
Conversation
|
I'm a bit hesitant on this one, might have significant security implications IMHO. But at the same time, we definitely need that for these ZIMs to work offline. |
c27024c to
390a545
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #392 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 42 42
Lines 2952 2952
Branches 452 452
=========================================
Hits 2952 2952 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@rgaudin WDYT about this? Pretty sure these crypto functions are used in other websites, so adding support for them is pretty important IMHO, and in most cases they are not used for anything which needs them to be "secure". But there will inevitably be one case in few years where we've completely forgot about this and it will bit us. I feel like the added value is way superior to the risk (especially since it is used on MDN which is an important ZIM to us), but I might be biased. If we agree this is worth-it, I will open a follow-up issue to identify all "common" functions which might be hidden in our "unsecure" context but useful to JS scripts. This might be only the beginning of something larger. And of course we can imagine we add a flag to add these "polyfills" conditionally, should it add any added value in some context. |
crypto.randomUUID() is restricted by browsers to secure contexts (HTTPS, or localhost). ZIM content is routinely served over plain HTTP from a non-localhost host (LAN server, offline hotspot, a VPN hostname, ...), which is not a secure context, so the browser omits randomUUID entirely and any page depending on it throws instead of running. crypto.getRandomValues() has no such restriction. Polyfill randomUUID on top of it (standard RFC 4122 v4 construction) whenever it's missing, as a side effect of loading wombatSetup.js, which already runs before any page script (it's injected first in warc2zim's head_insert.html). Found via openzim/warc2zim#327: MDN's "Try it" live-sample widgets use crypto.randomUUID() to build a session id, and silently failed for a user testing a ZIM over a Tailscale hostname (plain HTTP, non-localhost). (#391)
Like crypto.randomUUID(), crypto.subtle is restricted to secure contexts, but unlike randomUUID there is no lower-level primitive to build it from - digest() needs an actual hash implementation. Add a small, dependency-free digest(algorithm, data) polyfill supporting SHA-1 and SHA-256, the two algorithms observed in use (our own vendored wombat.js optionally uses SHA-1 for response ETags; archived pages have been seen using SHA-256 to derive a content id), matching the native SubtleCrypto.digest() -> Promise<ArrayBuffer> contract. Installed on crypto.subtle the same way, only when missing. sha1Bytes/sha256Bytes are cross-validated against Node's own crypto module across many input lengths, including the Merkle-Damgard padding's block-boundary edge cases, rather than relying solely on hand-transcribed test vectors (one of which did turn out to have a dropped a character - sha1Bytes itself was correct throughout). Found via openzim/warc2zim#327: after the randomUUID polyfill, MDN's live-sample iframes were still stuck at src="about:blank" with no visible error, because the code computing their content id awaits crypto.subtle.digest("SHA-256", ...), which silently never resolves when crypto.subtle is undefined. (#393)
390a545 to
394b63e
Compare
Summary
Both
crypto.randomUUID()andcrypto.subtle(SubtleCrypto) are restricted by browsers to secure contexts (HTTPS, orlocalhost). A ZIM served over plain HTTP on a non-localhosthost (LAN server, offline hotspot, a VPN/Tailscale hostname, ...) — an extremely common Kiwix deployment — is not a secure context, so the browser omits both entirely, and any page depending on them fails (randomUUIDthrows; code awaitingcrypto.subtle.digest()tends to fail silently instead).crypto.randomUUID():crypto.getRandomValues()has no such restriction, so it's polyfilled on top of it (standard RFC 4122 v4 construction, via a separately-testedrandomUUIDFromByteshelper).crypto.subtle.digest(): no lower-level primitive to build it from, so this adds a small, dependency-free pure-JSdigest(algorithm, data)supporting SHA-1 and SHA-256 — the two algorithms actually observed in use (our own vendoredwombat.jsoptionally uses SHA-1 for response ETags; archived pages have been seen using SHA-256 to derive a content id) — matching the nativeSubtleCrypto.digest()→Promise<ArrayBuffer>contract.Both are installed as a side effect of loading
wombatSetup.js, which already runs before any page script (warc2zim'shead_insert.htmlloads it first in<head>).sha1Bytes/sha256Bytesare cross-validated against Node's owncryptomodule across many input lengths, including the Merkle-Damgard padding's block-boundary edge cases, rather than relying solely on hand-transcribed test vectors — one of which did turn out to have a dropped character during review;sha1Bytesitself was correct throughout, confirmed via Python'shashlib.Fixes #391 and #393, found via openzim/warc2zim#327: after fixing #387/#389, a user testing over a Tailscale hostname (plain HTTP, non-localhost) hit
Uncaught TypeError: crypto.randomUUID is not a function, aborting the page's JS entirely. After therandomUUIDfix, MDN's live-sample iframes were still stuck atsrc="about:blank"with no visible error — traced tocrypto.subtlebeingundefined, silently breaking theawait crypto.subtle.digest("SHA-256", ...)call used to build their content id.Both reproduced directly (not just inferred): confirmed
window.isSecureContext === falseandtypeof crypto.randomUUID !== "function"/typeof crypto.subtle === "undefined"against a real non-localhost URL, then confirmed both resolved after this patch, with the live-sample demos rendering again end to end.