Vestibule/BLOG.md

278 lines
13 KiB
Markdown
Executable File

# From IEXPLORE.EXE to Rust: Rewriting a 2001 Kiosk Browser
**By Jeremy Anderson** — [dcos.net](https://dcos.net) — info@dcos.net
In December 2001, I was eighteen, coding on co-op while in school
for my first employer — a Boston-area MSP and programming company.
The company serviced medical offices, and one recurring problem was
the lobby computer: patients waiting for appointments would sit down
at it and immediately start looking for things they should not be
looking at, or worse, closing the browser and poking around the rest
of Windows. The receptionists were tired of babysitting it. My job
was to make the computer bulletproof enough to leave alone.
I shipped a solution in Visual Basic 6. It was, in retrospect, a
remarkable little artifact of late-90s teenage ingenuity — and
twenty-five years later, I decided to rewrite it properly.
## What the original did
The genius of the 2001 version was brute force. It embedded the
Internet Explorer COM control (`SHDocVw.dll`) in a maximized VB6 form
with `ControlBox = False` and `WindowState = 2` (maximized). It called
`SetWindowPos` with `HWND_TOPMOST` to keep the window on top of
everything. And — this is the part that made it actually work — it
called `SystemParametersInfo(97, True)`, the legendary undocumented
`SPI_SCREENSAVERRUNNING` flag that convinced Windows 9x it was running
a screensaver, which had the side effect of disabling Ctrl+Alt+Del,
Alt+Tab, and the Windows key.
That alone was not enough. A determined user could still close the VB6
app and reach the desktop. So the app did something beautifully
ruthless: on startup, it copied `C:\Program Files\Internet
Explorer\IEXPLORE.EXE` to `IEXPLORE.bak`, then deleted the original.
If the user managed to escape the kiosk, they would find no browser to
escape to. On unlock, the app copied the backup back into place.
The unlock code lived in plaintext — hardcoded in the form source of
the lobby build, read from `C:\windows\system\smt.txt` by the
earliest iteration. The UI was black background,
white text, Comic Sans. The menu was invisible by default and appeared
only when the mouse touched the top strip of the screen. The
delay loop before unlock was a raw `GoTo` counting loop, a CPU-burner
that pegged the processor at 100% for half a second.
The scrubbed source of all three iterations ships with this repository
under `history/vb6-2001/`.
It ran on Windows 95, 98, 98 SE, and ME. It explicitly refused to run
on NT, 2000, or XP because `SPI_SCREENSAVERRUNNING` does nothing on
NT-family kernels.
It worked. It ran in that lobby for years.
## Why rewrite it
The original program is unusable today for a list of reasons:
- It is VB6. The toolchain is dead. The runtime ships in Windows 11
only for legacy compatibility, and Microsoft has announced its
removal.
- It depends on `SHDocVw.dll`, which is Internet Explorer. IE is gone.
- `SPI_SCREENSAVERRUNNING` was a Win9x-only trick. Modern Windows
(NT kernel) ignores it entirely.
- The IEXPLORE.EXE shell game is impossible on modern Windows due to
filesystem permissions and Windows Resource Protection.
- Plaintext unlock-code storage — hardcoded in the form source, or a
plaintext file on disk — is indefensible.
- The Comic Sans UI is accessibility-hostile.
But the core idea — a public-facing browser that holds the perimeter
and never leaks session data between users — is more relevant than
ever. Medical lobbies still have lobby computers. Libraries still have
public terminals. Trade shows still have demo kiosks. The problem
shape has not changed; only the implementation needs to.
## The architectural decisions
A modern kiosk browser has to answer four questions:
1. **What renders the web content?**
2. **How is the perimeter enforced?**
3. **How is the operator UI protected?**
4. **How is session data sanitized?**
### Rendering: LibreWolf ESR
The constraint was no Chromium. Tauri was rejected because on Windows
it uses WebView2, which is Edge, which is Chromium. Electron was
rejected for the same reason and for its 150 MB binary size. Servo was
rejected because its web compat is still too inconsistent for
arbitrary patient-portal content.
LibreWolf ESR is the right answer. It is Gecko-based (no Chromium),
MPL 2.0 licensed, community-maintained, tracks Firefox ESR for
security, has Mozilla telemetry stripped, and ships enterprise
`policies.json` support for lockdown. It also navigated Mozilla's
trademark policy already, which means redistribution is straightforward.
### Perimeter: OS-level lockdown, not app-level
The original VB6 fought the OS from inside the app. It used Win32 API
calls to disable Ctrl+Alt+Del. It renamed system executables. This was
necessary on Windows 9x because the OS had no kiosk mode.
Modern operating systems do. Windows 10+ has AssignedAccess, which
configures a device as a dedicated kiosk at the OS level — single
auto-logon account, no shell, no escape. Linux has `cage`, a 3 MB
wlroots-based compositor that boots straight into a single client with
no window manager, no shell, no desktop.
The right architecture moves the perimeter enforcement out of the app
and into the OS, where it belongs. Vestibule does not try to disable
Ctrl+Alt+Del from inside the browser. AssignedAccess does that at the
session level, far more reliably than any app-level hook ever could.
### Operator UI: extension + Rust helper
The browser-level logic — URL filtering, hidden menu, session reset,
unlock dialog — lives in a WebExtension. This is the natural unit of
composition for Firefox-family browsers and gives us `webRequest`
blocking, `browsingData` sanitization, and `idle` detection for free.
The OS-level logic — password verification, power event detection,
signaling the supervisor — lives in a tiny Rust binary called `usher`.
It speaks Firefox Native Messaging (a stable, supported stdio-JSON
protocol) to the extension. The binary is 1.2 MB. The extension is
6 KB. Together they replace what would have been a 150 MB Electron
bundle.
### Session sanitization: three layers, defense in depth
The product contract is: a kiosk must never persist patient data
across sessions. Three enforcement layers:
1. **Block all save paths.** `policies.json` disables form history,
password manager, master password creation, Pocket, Screenshots,
Firefox Accounts, search suggestions, extension installs, popup
blocking override, and protocol handler registration.
`SanitizeOnShutdown` wipes everything on browser exit.
2. **Reset on trigger events.** The extension's `resetSession()`
invokes `browser.browsingData.remove()` with all eleven data types
on idle timeout, wake-from-sleep, unlock, and manual reset.
3. **OS-level defense.** AssignedAccess and `cage` ensure the browser
cannot be closed or escaped. The dedicated `vestibule-profile`
directory isolates the kiosk from any user profile data.
## The 2001 gestures that survived
Two original design decisions carry forward unchanged.
**The top-edge hidden menu.** The original VB6 form toggled its menu
in `picAddress_MouseMove`, based on `Y <= 10` (twips — effectively the
top edge of the screen). The modern equivalent is a content script
that listens for `mousemove` and shows a fixed-position overlay when
`clientY <= 3` pixels. Same gesture. Same UX rationale: the menu is invisible
by default, appears on a deliberate gesture, and disappears on
mouse-leave. Patients never see it. Operators always find it.
**The password unlock.** The original prompted for an unlock code and
compared it to the contents of `smt.txt`. The modern equivalent
prompts for an unlock code and verifies it against an Argon2id hash
stored at 0600 by usher. The flow is identical; the
storage and verification are not.
## What did not survive
**The IEXPLORE.EXE shell game.** Modern Windows filesystem permissions
make this impossible. The replacement is a URL allowlist enforced via
`webRequest.onBeforeRequest` in the extension. Stronger, safer, and
does not require renaming system executables.
**The CPU-melting delay loop.** The original used a `GoTo` counting
loop (`beginn: time = time + 1 ... GoTo beginn`) to burn half a second
before unlock. The modern
equivalent is `tokio::time::sleep(Duration::from_millis(500))`. Same
delay, 0% CPU.
**Plaintext password storage.** usher stores the Argon2id hash
(64 MiB memory cost) in a file owned and readable only by the kiosk
user — mode 0600 on Linux, default user ACL on Windows. Keyring
daemons are unavailable on minimal kiosk compositors like cage, so
file permissions are the enforcement boundary. The password itself is
never written anywhere.
**The Comic Sans UI.** The hidden menu uses system fonts
(`-apple-system, "Segoe UI", system-ui, sans-serif`). A retro 2001
theme is a Phase 5 option for those who want the nostalgia.
## Power state awareness
The original VB6 did not handle sleep because Windows 9x kiosks
typically did not sleep. Modern hardware does. Laptops used as kiosks
suspend on lid close. Mini-PCs suspend on idle. Tablets suspend on
inactivity.
Vestibule subscribes to the OS's power events. On Linux, usher
connects to D-Bus and listens for
`org.freedesktop.login1.Manager.PrepareForSleep(false)`, which fires
about two seconds after the system wakes. On Windows, usher registers
for `WM_POWERBROADCAST` and listens for `PBT_APMRESUMEAUTOMATIC`.
On wake, usher emits a `Wake` event to the extension, which calls
`resetSession("wake:suspend")`. The same path fires on idle timeout.
Both go through one code path, so behavior is identical regardless of
trigger.
The decision to detect rather than block sleep is deliberate.
Blocking sleep fights the OS — it causes battery drain on mobile
kiosks, thermal issues on fanless hardware, and interferes with
Windows Update overnight reboots. The OS power profile is the right
place to decide whether a device sleeps; that is a deployment-time
decision, not an app-runtime one. Vestibule reacts correctly when the
OS does sleep, and stays out of the way when it does not.
## What I learned doing this
The 2001 version took me about three weeks of evenings. I knew nothing
about Win32 API calls, COM interop, or filesystem security. I learned
all of it by reading `Declare Function` examples on Planet Source
Code and copying patterns I did not fully understand. The code was
procedural, repetitive, and full of `On Error GoTo` cascades that
existed to patch around the IEXPLORE.EXE rename failing halfway. It
worked because the problem was small and the OS was permissive.
The 2026 version took about a week of focused work. I know Rust, I
know Firefox extension APIs, and I know what the OS primitives
actually do. The code is threaded, table-driven, and structured around
a message-passing channel that makes the data flow obvious. It works
because the abstractions are right, not because I patched around
enough edge cases.
The instinct that survived intact is the one the original reviewer
called out: dig underneath the standard UI and manipulate the
environment directly. The 2001 version did this with Win32 API calls.
The 2026 version does it with D-Bus subscriptions, enterprise policy
files, and a message-passing channel between browser and helper. The
implementation is completely different. The instinct is the same.
## What comes next
Vestibule 1.2 ships the working core: real Argon2id unlock with file
storage at 0600, a dedicated unlock popup window, Windows power event
detection, the `librewolf.overrides.cfg` that selectively relaxes
LibreWolf's resistFingerprinting for SSO flows, per-origin preservation
for the data persistence allowlist — and, on top of that, the OS-level
provisioning wizards (an AssignedAccess setup wizard on Windows with a
Shell Launcher step-down, a `cage` systemd unit on Linux where the unit
IS the graphical session — no display manager at all), full
deprovisioning on both platforms, and the packaging itself: Inno Setup
on Windows and a Flatpak on Linux that acts as a self-describing
deployment kit. Since 1.2.2 the perimeter also extends to the web
itself: the URL policy blocks every domain by default and opens only
what the operator safelists — domain-based matching, with the home
page guaranteed reachable so the kiosk can never lock itself out.
Phase 3 adds crash auto-restart integration testing, telemetry hooks,
and the `always-on` power-inhibit mode. Later phases: Flathub
distribution, remote management, and the retro 2001 theme.
One platform decision is already settled: Vestibule targets Linux
first. The Windows stack works and stays CI-validated, but its
binaries ship unsigned — a code-signing certificate is an unfunded
line item unless a donation covers it — and Linux has no signing
gate to care about.
Phase 5+, deferred: webcam presence detection. When a webcam is
present, usher captures a baseline face embedding at session start and
watches for two conditions: no face visible for N seconds (user
walked away) or face embedding mismatch (different user approached).
Either triggers an immediate session reset, faster than the idle
timeout. Privacy review precedes any biometric code. Frames are
processed in-memory only; embeddings never leave the local process;
the webcam LED is respected.
*Jeremy Anderson, 2026. [dcos.net](https://dcos.net). info@dcos.net.*