Client-Side Runtime CompositionTechnical Reference
This page covers the technical implementation of the browser runtime composition system: how each component works, how data changes shape as it moves through the system, how the cryptographic chain fits together, and where observable signals remain for defenders. For the product thinking and design rationale, start with the narrative.
System Overview
The system is a two-layer web application. The visible layer is a normal React application that boots, registers a service worker, and renders a public interface. The second layer is an encrypted payload that can be unlocked locally, decrypted in the browser, and rendered inside a sandboxed iframe without fetching any additional files from the network.
The visible host must be a substantial, data-heavy application in its own right. A service worker sitting idle on a simple static page would look unusual under inspection. A service worker that actively caches and serves a rich web application (the kind that works offline, sometimes called a progressive web app) is expected behaviour. The host justifies the application’s size, memory footprint, and service worker registration simply by being a credible product.
Four components make this work:
| Component | Runs | Role |
|---|---|---|
| Payload Compiler | Build time (Node.js) | Scans payload folders, encodes files to base64, encrypts each namespace as a separate AES-256-GCM bundle |
| Runtime Controller | Browser (React) | Monitors input for the trigger, derives the decryption key, manages unlock, conceal, reveal, and purge states |
| Runtime Service Worker | Browser (Service Worker) | Holds the decrypted virtual file system in memory and serves /vfs/... requests as synthetic responses |
| Host Application | Browser (React) | The visible shell: boots the app, registers the worker, imports the encrypted bundles, wraps the layout in the controller |
The components are organised as a monorepo with workspaces:
| Path | Purpose |
|---|---|
packages/payload-compiler | Node.js CLI that produces encrypted payload bundles |
packages/runtime-controller | React component, hooks, and crypto utilities |
packages/runtime-service-worker | Service worker source and build script |
apps/tech-archive-host | Example host application that assembles the capabilities |
Execution Timeline
The system moves through ten steps from initial page load to a rendered isolated runtime. Each step is a handoff: one component finishes its job and passes the result to the next.
/vfs/launcher/index.html) to their base64-encoded contents.
postMessage. The worker stores it in a JavaScript Map in RAM, not in any persistent storage mechanism.
/vfs/... requests are intercepted
When the iframe asks for a file, the worker checks its memory, finds the matching entry, converts it from the base64 text format back into the binary data the browser expects, figures out what type of file it is (HTML, CSS, JavaScript, image), and hands it back as a normal browser response with Cache-Control: no-store headers. Missing paths receive an explicit 404 from the worker rather than falling through to the network.
/vfs/launcher/index.html. From the iframe’s perspective, the virtual files behave like ordinary hosted web assets.
postMessage to the parent), the controller decrypts only that additional bundle and appends it to the worker’s memory map. The iframe navigates to the new /vfs/... route.
Representative code
These snippets illustrate three key moments in the flow. They are simplified for clarity.
// Worker registration (host app startup)
navigator.serviceWorker.register("/stealth-runtime-sw.js");
// Key derivation (controller, after trigger match)
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" },
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
// Fetch interception (service worker)
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/vfs/") && volatileVfsMap) {
event.respondWith(serveFromRAM(url.pathname));
}
});
Component Contracts
Each component has a strict boundary: what it owns, what it produces, and what it deliberately avoids. These boundaries are what keep the system modular.
Payload Compiler
| Owns | File discovery, base64 encoding, VFS construction, AES-256-GCM encryption per namespace |
| Input | A source directory with one subdirectory per payload namespace, plus a trigger key string |
| Output | One encrypted bundle per namespace, each containing salt, iv, authTag, and ciphertext |
| Avoids | Any knowledge of the browser, the runtime, or visibility state |
Runtime Controller
| Owns | Trigger detection (suffix hash search), key derivation, decryption, iframe lifecycle, visibility state (conceal, reveal, purge) |
| Input | Keyboard events plus encrypted payload bundles |
| Output | Decrypted VFS objects sent to the worker, plus the mounted iframe overlay |
| Avoids | Serving files directly; the controller hands off to the worker and steps back |
Key source files: StealthWrapper.tsx (main wrapper), useEnvironmentInput.ts (trigger hook), useDecryption.ts (decryption hook), crypto.ts (hex conversion utilities).
Runtime Service Worker
| Owns | In-memory VFS storage, /vfs/... fetch interception, base64-to-binary conversion, MIME type inference, response headers |
| Input | Hydrated VFS maps via postMessage (HYDRATE_VFS, HYDRATE_VFS_APPEND, PURGE_VFS) |
| Output | Synthetic Response objects with correct content types and Cache-Control: no-store, no-cache, must-revalidate |
| Avoids | Any decision about when content is trusted or allowed to unlock; it only stores and serves what it receives |
The memory-only design is deliberate. The worker never writes decrypted content to IndexedDB, Cache Storage, or any other persistent mechanism. When the worker is terminated or purged, the data is gone. This means the runtime exists only for as long as it is actively being used.
Host Application
| Owns | The visible UI, service worker registration, encrypted data import, and controller integration |
| Input | The compiled encrypted payload file (imported as application data) |
| Output | A complete, functioning public-facing application that wraps the controller |
| Avoids | Understanding every cryptographic step or file-serving branch; it assembles rather than implements |
Data Transformations
The payload changes form five times as it moves through the system. Each transformation moves the data closer to something the browser can render.
| Transformation | Where it happens | What changes |
|---|---|---|
| File to base64 | Compiler (build time) | Binary file contents become text-safe strings that can live inside JSON |
| VFS to ciphertext | Compiler (build time) | The JSON map is serialised, then encrypted with a key derived from the trigger phrase |
| Ciphertext to plaintext | Controller (browser) | The browser derives the same key and reverses the encryption, producing the original VFS map |
| Plaintext to memory entries | Service worker (browser) | The JSON map is received via postMessage and stored in a JavaScript Map |
| Memory to responses | Service worker (browser) | Base64 entries are decoded to binary, paired with MIME types, and returned as Response objects |
Cryptographic Design
The cryptographic chain serves two purposes: it keeps the payload unreadable until the correct trigger is supplied, and it verifies that the payload data has not been altered. The system uses standard Web Crypto API primitives throughout.
At a high level, the trigger phrase becomes a key, and that key unlocks the payload. PBKDF2 is used so the browser derives proper key material instead of treating the raw phrase itself as the key. AES-GCM is used because it provides both confidentiality (the data is unreadable without the key) and integrity (the browser can reject altered payload data rather than rendering it). Each encrypted bundle includes the small pieces of metadata the browser needs to reverse the process: a salt (randomness added during key derivation), an initialisation vector (randomness added during encryption), and an authentication tag (a check that confirms the data has not been tampered with).
Key derivation (PBKDF2)
The trigger phrase is the starting material for key derivation. Rather than using the phrase directly as an encryption key, it is passed through PBKDF2 (Password-Based Key Derivation Function 2), which turns a human-readable phrase into cryptographic key material. This adds intentional computational cost, making brute-force guessing slower, and it means the raw phrase is never used as a key directly.
| Parameter | Value | Purpose |
|---|---|---|
| Algorithm | PBKDF2 | Derives a strong key from a human-readable phrase |
| Hash | SHA-256 | The hash function used internally during derivation |
| Salt | Random (per encryption) | Ensures that encrypting the same payload with the same phrase produces different output each time |
| Output | AES-256-GCM key | 256-bit symmetric key used for encryption and decryption |
Encryption (AES-256-GCM)
AES-GCM (Galois/Counter Mode) provides both confidentiality and integrity. Confidentiality means the data is unreadable without the key. Integrity means the browser can detect if the ciphertext has been tampered with, and will reject it rather than producing a corrupted result.
| Parameter | Purpose |
|---|---|
| Initialisation vector (IV) | Unique randomness per encryption operation, so the same plaintext produces different ciphertext each time |
| Authentication tag | A checksum that lets the browser verify the ciphertext is intact before decrypting |
| Ciphertext | The encrypted VFS data |
The encrypted bundle stores all four values together: salt, IV, authentication tag, and ciphertext. That is everything the browser needs to derive the same key and reverse the encryption.
Trigger verification (SHA-256)
The trigger phrase is stored in the source code as a hash: a one-way fingerprint produced by running the phrase through a mathematical function (SHA-256). You can produce the fingerprint from the phrase, but you cannot reverse it to recover the phrase. That means someone inspecting the application’s code would find only the fingerprint, and working out what produced it requires already knowing the answer.
On every key press, the controller tests every possible suffix of the current input buffer by hashing each one with SHA-256 and comparing the result to the stored fingerprint. This suffix search means the trigger phrase can be recognised regardless of what was typed before it.
Lifecycle States
The system moves through six states during a complete session. The transitions are deterministic: each state leads to the next through a specific event.
/vfs/... requests.
Two intermediate behaviours sit alongside these states:
- Conceal (blur or tab switch): The iframe overlay is hidden, but the worker retains its memory and the runtime stays loaded in the background. The system remains in the Serving state, just visually concealed.
- Reveal (trigger re-entered while concealed): The overlay becomes visible again without repeating decryption or re-hydrating the worker. The system was already Serving; it simply shows the iframe again.
Failure Modes
Each step in the execution chain has a specific failure condition. The system is designed to fail silently: a wrong trigger produces no error message, a corrupted payload is rejected cleanly, and a missing worker simply means the virtual files cannot be served.
| Condition | What happens | Why |
|---|---|---|
| Wrong trigger phrase | The hash comparison never matches, so the unlock path never starts | The controller compares hashes on every keystroke; a mismatch is the normal state |
| Key derivation failure | Decryption cannot begin; the system stays dormant | PBKDF2 depends on the Web Crypto API being available and the parameters being correct |
| Payload corruption or tampering | AES-GCM rejects the ciphertext during decryption | The authentication tag verification fails, so the browser refuses to produce output rather than returning corrupted data |
| Service worker missing | The controller may decrypt successfully, but the virtual files cannot be served through /vfs/... | Without the worker’s fetch interception, the iframe’s requests have nowhere to resolve |
| Worker state cleared by browser | The in-memory VFS disappears; the runtime stops working | Memory-only storage means the data is gone whenever the browser decides to terminate the worker |
| Tab switch or window blur | The overlay hides; the runtime stays loaded in the background | This is intentional concealment, not a failure, but the user must re-enter the trigger to see the runtime again |
| Escape key (panic purge) | The iframe unmounts, the controller resets, and the worker empties its memory | This is the deliberate full-purge path; the system returns to monitoring |
Observable Signals
This architecture shifts where visibility sits. Visibility persists across every layer. Every capability used is a standard browser feature, and standard browser features leave traces that defenders, monitoring tools, and browser developer tools can inspect.
| Layer | What remains observable | Why it matters |
|---|---|---|
| Network | Unusually large initial download relative to the visible site’s purpose; browsing sessions that go quiet after the first transfer | The encrypted payload ships with the initial page load, inflating the bundle size beyond what the visible application would justify on its own |
| JavaScript runtime | Calls to crypto.subtle.deriveKey and crypto.subtle.decrypt; CPU spikes during key derivation | PBKDF2 is intentionally expensive and AES-GCM decryption produces measurable computation; endpoint monitoring can profile this |
| Memory | Base64 decoding, object creation, and Blob construction leave transient artefacts visible in heap snapshots | Decrypted content exists in memory for as long as the runtime is active; memory profiling tools can capture it |
| Service worker | Worker registration, fetch interception patterns, and synthetic responses for /vfs/... paths | Service workers are inspectable through chrome://serviceworker-internals, the Application panel in DevTools, and organisational policy enforcement |
| UI behaviour | Iframe overlays appearing and disappearing, reveal and conceal cycles, interaction patterns inconsistent with the visible shell | Both automated screen-capture tools and human review can detect usage patterns that diverge from the public application’s expected behaviour |
Inspecting in DevTools
The system can be examined directly using standard browser developer tools:
- Application panel: inspect service worker registration, activation status, and scope
- Network panel: watch for
/vfs/...requests appearing after the unlock sequence, and note that responses come from the service worker rather than the network - Memory panel: take heap snapshots while the runtime is active to see where decrypted content and hydrated objects sit in memory
- Sources panel: step through the controller and worker source to follow the unlock path
Defensive Controls
Organisations and security teams retain practical leverage over this architecture at multiple layers.
| Control | What it addresses |
|---|---|
| Service worker origin allowlisting | Restrict which domains are permitted to register service workers, preventing untrusted sites from using fetch interception |
| Aggressive worker state clearing | Clear service worker caches and state on session exit, ensuring memory-backed content is cleared between visits |
| Iframe policy restrictions | Limit iframe capabilities (sandboxing attributes, Content Security Policy) on untrusted or unreviewed sites |
| Endpoint crypto profiling | Monitor crypto.subtle usage patterns and correlate with user behaviour to flag unexpected key derivation or decryption activity |
| Network payload analysis | Flag mismatches between a site’s visible purpose and its initial download size or traffic shape |
| Session behaviour correlation | Cross-reference time-on-page, focus changes, and interaction patterns with what the visible application would justify |
Defensive framing: This architecture moves where visibility occurs, shifting signals between network, runtime, memory, and UI layers. A layered defensive approach that watches across all these surfaces retains clear leverage.
Performance and Tradeoffs
Costs of this design
- PBKDF2 computation: key derivation adds intentional CPU work during unlock. This is a deliberate security property (slowing brute-force attacks) but it also means the unlock is perceptibly slower than a simple password check.
- Memory pressure: base64 decoding, Blob construction, and hydrating the worker’s in-memory map all consume transient memory. For large payloads this becomes a meaningful cost.
- Service worker hop: every virtual file request passes through the worker’s fetch handler, adding a processing step between the iframe’s request and the response. For small assets this is negligible; for many assets it accumulates.
- Bundle size inflation: the encrypted payload ships with the initial page load, increasing the host application’s total download size. The host must be substantial enough to justify this.
Alternatives considered
| Alternative | Tradeoff |
|---|---|
| Fetch the payload later (on demand) | Reduces initial bundle size but creates an obvious network event at unlock time, making the second-stage load clearly visible |
| Use IndexedDB for storage | Would preserve the runtime across page reloads but weakens the memory-only design; persistent storage is more inspectable and leaves durable forensic artefacts |
| Use WebAssembly for the runtime | Could change execution characteristics but would not remove observability; the loading, compilation, and execution of Wasm modules is itself an observable event |
| Render into the main DOM instead of an iframe | Would remove the iframe boundary but collapse the isolation between the host and the runtime, making both harder to manage and easier for DOM-scraping tools to inspect |