Add browser vault and static Qt bundle support
ober
a667d013baef3eafa09250c272b68e35c0c8742a
--- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,9 @@ jerboa-browser jerboa-browser-static-qt-linux-amd64.tar.gz *.boot *.so +*.dylib *.wpo jb-main.c jb_*.h +scheme/browser/build-vault-secret.ss .DS_Store --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # CMake build trees build/ **/build/ +**/target/ # Native binary + Jerboa build artifacts (see build-binary.ss / Makefile) /jerboa-browser @@ -11,6 +12,7 @@ jerboa-browser-static-qt-linux-amd64.tar.gz *.boot jb-main.c jb_*.h +scheme/browser/build-vault-secret.ss # Offline Qt harness output (make test-gui) + CI snapshot bundle test-artifacts/ @@ -22,3 +24,4 @@ gui-snapshots.tar.gz # local dev: symlink to the Rust crypto lib so `jerbuild exec` (interpreter # tests) can load it; the built binary registers these symbols statically. lib/libjerboa_native.* +lib/libyubikey_native.* --- a/.jerbuild +++ b/.jerbuild @@ -6,4 +6,6 @@ (entry "scheme/browser-main.ss") (output "jerboa-browser") -(libdirs "scheme") +(libdirs "vendor/jerboa-yubikey/lib" "scheme" "vendor/jerboa-fuse/lib") +(pre-build "sh support/ensure-vendor.sh jerboa-yubikey https://git.sr.ht/~lisp/jerboa-yubikey") +(pre-build "sh support/gen-browser-vault-secret.sh scheme/browser/build-vault-secret.ss") --- a/Dockerfile.qt +++ b/Dockerfile.qt @@ -1,11 +1,11 @@ -# Docker recipe for building the jerboa-browser Linux Qt artifact. +# Docker recipe for building the bundled jerboa-browser Linux Qt artifact. # # This mirrors the jerboa-emacs static-qt workflow: build inside Docker, create a # container, and copy the finished artifact from /out. Unlike jemacs-qt, this # project uses Qt WebEngine, so the result is not a single fully static Qt -# executable. The tarball contains the native Jerboa browser binary plus the -# Qt WebEngine backend shared library and expects a system Qt 6 WebEngine -# runtime, matching packaging/linux/build-tarball.sh. +# executable. The tarball contains the native Jerboa browser binary, the Qt +# WebEngine backend shared library, Qt libraries/plugins, QtWebEngineProcess, +# and WebEngine resource files. # # Usage: # docker build -f Dockerfile.qt -t jerboa-browser-qt-builder . @@ -23,7 +23,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ file \ g++ \ libgl1-mesa-dev \ + libudev-dev \ make \ + patchelf \ pkg-config \ qt6-base-dev \ qt6-webengine-dev \ @@ -48,7 +50,7 @@ RUN if ! command -v git >/dev/null 2>&1; then \ COPY . /src WORKDIR /src -RUN SCHEME="$(command -v scheme)" bash packaging/linux/build-tarball.sh && \ +RUN BUNDLE_QT=1 SCHEME="$(command -v scheme)" bash packaging/linux/build-tarball.sh && \ mkdir -p /out && \ cp qt-webengine/build/jerboa-browser-*-linux-*.tar.gz \ /out/jerboa-browser-static-qt.tar.gz && \ @@ -57,10 +59,39 @@ RUN SCHEME="$(command -v scheme)" bash packaging/linux/build-tarball.sh && \ tar tzf "$tb" | grep -E '/bin/jerboa-browser\.bin$' >/dev/null && \ tar tzf "$tb" | grep -E '/lib/libjerboa_browser\.so$' >/dev/null && \ tar tzf "$tb" | grep -E '/lib/libjerboa_native\.so$' >/dev/null && \ + tar tzf "$tb" | grep -E '/bin/qt.conf$' >/dev/null && \ + tar tzf "$tb" | grep -E '/lib/libjerboa_fuse_mount\.so$' >/dev/null && \ + tar tzf "$tb" | grep -E '/lib/libyubikey_native\.so$' >/dev/null && \ + tar tzf "$tb" | grep -E '/lib/libQt6WebEngineCore\.so\.6$' >/dev/null && \ + tar tzf "$tb" | grep -E '/libexec/QtWebEngineProcess$' >/dev/null && \ + tar tzf "$tb" | grep -E '/libexec/qt.conf$' >/dev/null && \ + tar tzf "$tb" | grep -E '/resources/qtwebengine_resources\.pak$' >/dev/null && \ bin_path="$(tar tzf "$tb" | grep -E '/bin/jerboa-browser\.bin$' | head -n 1)" && \ tar xzf "$tb" -O "$bin_path" > /tmp/jerboa-browser && \ file /tmp/jerboa-browser FROM ubuntu:24.04 COPY --from=builder /out/jerboa-browser-static-qt.tar.gz /out/jerboa-browser-static-qt.tar.gz +RUN set -eux; \ + mkdir -p /tmp/check; \ + tar xzf /out/jerboa-browser-static-qt.tar.gz -C /tmp/check; \ + appdir="$(find /tmp/check -mindepth 1 -maxdepth 1 -type d -name 'jerboa-browser-*' | head -n 1)"; \ + test -x "$appdir/bin/jerboa-browser"; \ + test -x "$appdir/bin/jerboa-browser.bin"; \ + test -f "$appdir/bin/qt.conf"; \ + test -f "$appdir/lib/libjerboa_browser.so"; \ + test -f "$appdir/lib/libjerboa_native.so"; \ + test -f "$appdir/lib/libjerboa_fuse_mount.so"; \ + test -f "$appdir/lib/libyubikey_native.so"; \ + test -f "$appdir/lib/libQt6WebEngineCore.so.6"; \ + test -x "$appdir/libexec/QtWebEngineProcess"; \ + test -f "$appdir/libexec/qt.conf"; \ + test -f "$appdir/resources/qtwebengine_resources.pak"; \ + test -f "$appdir/plugins/platforms/libqxcb.so"; \ + test -f "$appdir/plugins/platforms/libqoffscreen.so"; \ + : > /tmp/ldd.txt; \ + for f in "$appdir/bin/jerboa-browser.bin" "$appdir/lib/libjerboa_browser.so" "$appdir/lib/libjerboa_native.so" "$appdir/lib/libjerboa_fuse_mount.so" "$appdir/lib/libyubikey_native.so" "$appdir/libexec/QtWebEngineProcess"; do \ + LD_LIBRARY_PATH="$appdir/lib" ldd "$f" >> /tmp/ldd.txt 2>&1 || true; \ + done; \ + ! grep -q 'not found' /tmp/ldd.txt CMD ["cat", "/out/jerboa-browser-static-qt.tar.gz"] --- a/Makefile +++ b/Makefile @@ -3,9 +3,14 @@ # full Qt/WebKit GUI bundle additionally needs the native wrapper.) JERBUILD ?= jerbuild JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null) -JEXEC := $(JERBUILD) exec --libdirs $(CURDIR)/scheme:$(JH)/lib +JERBOA_YUBIKEY_URL ?= https://git.sr.ht/~lisp/jerboa-yubikey +JERBOA_YUBIKEY_DIR := vendor/jerboa-yubikey +JERBOA_YUBIKEY_LIB := $(JERBOA_YUBIKEY_DIR)/lib +JEXEC := $(JERBUILD) exec --libdirs $(CURDIR)/$(JERBOA_YUBIKEY_LIB):$(CURDIR)/scheme:$(CURDIR)/vendor/jerboa-fuse/lib:$(JH)/lib JERBUILD_ERROR := "ERROR: jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild." +CC ?= cc +CARGO ?= cargo DOCKER ?= docker DOCKER_PLATFORM ?= linux/amd64 DOCKER_BUILD_FLAGS ?= @@ -23,6 +28,10 @@ STATIC_QT_TARBALL ?= jerboa-browser-static-qt-linux-amd64.tar.gz # static-qt tarball bundles a copy in lib/; its launcher adds lib/ to # LD_LIBRARY_PATH so the binary finds it from any CWD.) SOEXT := $(if $(filter Darwin,$(shell uname -s)),dylib,so) +FUSE_SHARED_FLAG := $(if $(filter Darwin,$(shell uname -s)),-dynamiclib,-shared) +FUSE_SHIM := libjerboa_fuse_mount.so +BUILD_VAULT_SECRET := scheme/browser/build-vault-secret.ss +YUBIKEY_NATIVE_SHARED := $(JERBOA_YUBIKEY_DIR)/yubikey-native/target/release/libyubikey_native.$(SOEXT) # `make binary` links the static archive from a temp dir and leaves no shared # lib behind, so build the cdylib from jerbuild's bundled crate if it isn't # already cached, then symlink it where native-rust probes (./lib, CWD-relative). @@ -37,7 +46,25 @@ lib/libjerboa_native.$(SOEXT): | check-jerbuild if [ -f "$$src" ]; then ln -sf "$$src" "$@" && echo "staged $@ -> $$src"; \ else echo "note: libjerboa_native.$(SOEXT) unavailable; crypto tests may fail"; fi -.PHONY: all build binary run test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers repl clean help check-jerbuild check-docker static-qt docker-static-qt +$(FUSE_SHIM): vendor/jerboa-fuse/src/mount_helper.c + $(CC) $(FUSE_SHARED_FLAG) -fPIC -O2 -o $@ $< + +$(BUILD_VAULT_SECRET): + sh support/gen-browser-vault-secret.sh $@ + +vendor-yubikey: + sh support/ensure-vendor.sh jerboa-yubikey "$(JERBOA_YUBIKEY_URL)" + @test -f "$(JERBOA_YUBIKEY_LIB)/yubikey/auth.sls" || \ + { echo "ERROR: missing $(JERBOA_YUBIKEY_LIB)/yubikey/auth.sls" >&2; exit 1; } + +lib/libyubikey_native.$(SOEXT): vendor-yubikey + $(CARGO) build --manifest-path $(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml --release --no-default-features + @mkdir -p lib + cp -f $(YUBIKEY_NATIVE_SHARED) $@ + +yubikey-native: lib/libyubikey_native.$(SOEXT) + +.PHONY: all build binary run test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers repl clean help check-jerbuild check-docker static-qt docker-static-qt vault-secret rotate-vault-secret vendor-yubikey yubikey-native .DEFAULT_GOAL := help all: binary @@ -50,7 +77,7 @@ check-docker: { echo "ERROR: Docker executable '$(DOCKER)' not found. Install Docker or set DOCKER=/path/to/docker."; exit 1; } # Build the self-contained native ./jerboa-browser via .jerbuild. -binary: check-jerbuild +binary: check-jerbuild vendor-yubikey $(FUSE_SHIM) $(BUILD_VAULT_SECRET) $(JERBUILD) build @echo "" && ls -lh jerboa-browser && file jerboa-browser @@ -61,8 +88,8 @@ run: binary # The crypto-using interpreter suites load (std crypto native-rust), so stage the # Rust crypto lib (rule above) as a prerequisite of each. -test-securestore test-commands test-keys test-nav test-hint test-pass test-polish test-adblock: lib/libjerboa_native.$(SOEXT) -test test-keymap test-minibuffer test-gui test-buffers repl: check-jerbuild +test-securestore test-commands test-keys test-nav test-hint test-pass test-polish test-adblock: lib/libjerboa_native.$(SOEXT) vendor-yubikey +test test-keymap test-minibuffer test-gui test-buffers repl: check-jerbuild vendor-yubikey # Run the (browser) test suite in the interpreter (no binary needed). Depends on # the pure secure-store unit tests so they run as part of `make test`. @@ -139,9 +166,14 @@ test-buffers: repl: $(JEXEC) scheme/browser-repl.ss +vault-secret: $(BUILD_VAULT_SECRET) + +rotate-vault-secret: + sh support/gen-browser-vault-secret.sh --force $(BUILD_VAULT_SECRET) + # Docker wrapper matching the jerboa-emacs static-qt workflow. Qt WebEngine is -# not emitted as one fully static executable; the artifact is the existing Linux -# tarball shape with the native browser binary plus system-Qt WebEngine backend. +# not emitted as one fully static executable; the artifact is a bundled Linux +# tarball with the native browser binary plus the Qt WebEngine runtime. static-qt: docker-static-qt docker-static-qt: check-docker @@ -159,13 +191,33 @@ docker-static-qt: check-docker { echo "ERROR: launcher missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } @tar tzf $(STATIC_QT_TARBALL) | grep -E '/bin/jerboa-browser\.bin$$' >/dev/null || \ { echo "ERROR: native binary missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/bin/qt.conf$$' >/dev/null || \ + { echo "ERROR: Qt config missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } @tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libjerboa_browser.so$$' >/dev/null || \ { echo "ERROR: Qt WebEngine backend missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } @tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libjerboa_native.so$$' >/dev/null || \ { echo "ERROR: Rust crypto lib (libjerboa_native) missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libjerboa_fuse_mount.so$$' >/dev/null || \ + { echo "ERROR: FUSE vault helper missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libyubikey_native.so$$' >/dev/null || \ + { echo "ERROR: YubiKey native backend missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libQt6WebEngineCore.so.6$$' >/dev/null || \ + { echo "ERROR: Qt WebEngineCore runtime missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/libexec/QtWebEngineProcess$$' >/dev/null || \ + { echo "ERROR: QtWebEngineProcess missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/libexec/qt.conf$$' >/dev/null || \ + { echo "ERROR: QtWebEngineProcess Qt config missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/resources/qtwebengine_resources.pak$$' >/dev/null || \ + { echo "ERROR: Qt WebEngine resources missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/plugins/platforms/libqxcb.so$$' >/dev/null || \ + { echo "ERROR: Qt xcb platform plugin missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } + @tar tzf $(STATIC_QT_TARBALL) | grep -E '/plugins/platforms/libqoffscreen.so$$' >/dev/null || \ + { echo "ERROR: Qt offscreen platform plugin missing from $(STATIC_QT_TARBALL)" >&2; exit 1; } clean: rm -f jerboa-browser jb-main.c jb-main.o jb_*.h jerboa-browser.boot + rm -f $(FUSE_SHIM) + rm -f lib/libyubikey_native.$(SOEXT) rm -f $(STATIC_QT_TARBALL) rm -rf .bcache test-artifacts find scheme -name '*.so' -delete 2>/dev/null || true @@ -175,7 +227,11 @@ help: @echo "jerboa-browser — programmable Qt WebEngine pane from the Jerboa REPL" @echo "" @echo " make binary build the self-contained native ./jerboa-browser" - @echo " make static-qt build the Linux Qt/WebEngine tarball via Docker" + @echo " make static-qt build the bundled Linux Qt/WebEngine tarball via Docker" + @echo " make vault-secret create the local build vault secret, if missing" + @echo " make rotate-vault-secret replace the local build vault secret" + @echo " make vendor-yubikey clone jerboa-yubikey into vendor/" + @echo " make yubikey-native build lib/libyubikey_native for direct USB" @echo " make test run the (browser) test suite (interpreter)" @echo " make test-gui offline Qt GUI/snapshot harness (offscreen, headless)" @echo " make repl REPL with (browser) preloaded (interpreter)" --- a/README.md +++ b/README.md @@ -78,10 +78,11 @@ and the echo area reports the running count of blocked requests. ## YubiKey-encrypted bookmarks Bookmarks (not passwords) can be saved **encrypted at rest** and unlocked with a -YubiKey's PIV applet — no `ykman`, `pcscd`, or other external tool needed. The -PIV transport is built-in PC/SC (Apple's PCSC.framework on macOS, pcsc-lite on -Linux); the crypto and the minimal YubiKey library are vendored -(`scheme/browser/{secure-store,yubikey-unlock}.ss`, `scheme/yubikey/`). +YubiKey's PIV applet — no `ykman` or `yubico-piv-tool` needed. The crypto lives +in `scheme/browser/secure-store.ss` and YubiKey support comes from the vendored +`vendor/jerboa-yubikey/lib` library. On macOS it uses Apple's built-in +PCSC.framework; Linux packages bundle `libyubikey_native.so` for direct USB CCID +access, with pcsc-lite as a fallback when available. - `C-c y i` — **init**: provisions PIV slot 9D if empty (generates an RSA-2048 key — PIN-policy ALWAYS, touch CACHED — authenticating the default management @@ -97,11 +98,59 @@ key); only unlocking talks to the card. Store: `$XDG_DATA_HOME/jerboa-browser/ bookmarks.enc`. Needs a YubiKey 5 (firmware 5.3+); on Linux, install `pcsc-lite` and run `pcscd`. +## Encrypted browser vault + +By default, WebEngine still uses an off-record profile. To keep browser cache, +cookies, local storage, downloads, and other profile data in an encrypted FUSE +vault, launch with: + +```sh +JERBOA_BROWSER_VAULT=1 jerboa-browser +``` + +The browser process creates and mounts its own vault before the WebEngine +profile is opened, then points Qt at directories inside that mount. The FUSE +access controller trusts only the browser PID and descendants such as +`QtWebEngineProcess`; a `jsh` `,vault mount` can be used at a different path but +is not reused by the browser. Defaults: + +- Vault file: `$XDG_DATA_HOME/jerboa-browser/browser.vault` +- Mountpoint: `$XDG_RUNTIME_DIR/jerboa-browser/vault-<browser-pid>` (or `/tmp`) +- Size: `JERBOA_BROWSER_VAULT_BLOCKS` blocks, default 65536 (256 MiB) + +The production default is YubiKey PIV unlock. With `JERBOA_BROWSER_VAULT=1`, the +browser prompts for the YubiKey PIV PIN before the Qt profile opens and derives +the vault passphrase through the card operation, requiring the YubiKey, PIN, and +touch when the slot policy requires it. A stolen vault file plus browser binary +is not enough to open the profile. + +For first use with a blank PIV slot 9D, launch once with +`JERBOA_BROWSER_VAULT_YUBIKEY_PROVISION=1`; the browser will generate an +RSA-2048 key in the key-management slot using the default PIV management key, +matching the bookmark-store provisioning path. + +Explicit overrides are still available: + +- `JERBOA_BROWSER_VAULT_PASSPHRASE_FILE` — 0600 file containing a recovery or + test passphrase. +- `JERBOA_BROWSER_VAULT_PASSPHRASE` — local testing only. +- `JERBOA_BROWSER_VAULT_UNLOCK=prompt` — terminal passphrase prompt. +- `JERBOA_BROWSER_VAULT_UNLOCK=build-secret` — development fallback. + +The build-secret fallback is not production-grade: `make binary` creates +`scheme/browser/build-vault-secret.ss` if it is missing, and that generated, +ignored module holds a random build-local passphrase as masked bytes rather +than a printable string. Normal rebuilds preserve it; `make rotate-vault-secret` +replaces it. Anyone with the generated source or binary can still recover that +secret, so use it only for development or migration. + ## Build / test ```sh make binary # the self-contained native ./jerboa-browser -make static-qt # Linux Qt/WebEngine tarball via Docker (linux/amd64) +make static-qt # bundled Linux Qt/WebEngine tarball via Docker (linux/amd64) +make vendor-yubikey # clone https://git.sr.ht/~lisp/jerboa-yubikey into vendor/ +make yubikey-native # build lib/libyubikey_native for direct USB YubiKey access make test # (browser) suite + secure-store unit tests; also test-{keymap,…,polish} make test-securestore # just the pure secure-store crypto/format unit tests make test-gui # offline Qt snapshot harness (offscreen, headless) @@ -109,9 +158,10 @@ make test-gui # offline Qt snapshot harness (offscreen, headless) `make static-qt` mirrors the jerboa-emacs Docker workflow and copies out `jerboa-browser-static-qt-linux-amd64.tar.gz`. Because this backend is Qt -WebEngine, the artifact is a Linux tarball with the native browser binary plus -`libjerboa_browser.so`; it still expects a system Qt 6 WebEngine runtime rather -than a literal one-file static WebEngine executable. +WebEngine, the artifact is a bundled Linux tarball rather than a literal +one-file static executable. It includes the native browser binary, the +`libjerboa_browser.so` backend, the browser vault FUSE helper, Qt +libraries/plugins, `QtWebEngineProcess`, and WebEngine resource files. The functional suites run offscreen (`QT_QPA_PLATFORM=offscreen`) and hermetic (`JWB_TEST_NO_NETWORK=1`), modeled on the jerboa-emacs Qt test harness. @@ -125,6 +175,7 @@ qt-webengine/ Qt WebEngine backend (CMake; builds libjerboa_browser probe/ Ticket 0.1 standalone proof-of-host (kept for re-checks) tests/ ABI/handle tests (ctest) scheme/ Jerboa wrapper module + tests (.ss) +vendor/jerboa-fuse/ browser-owned encrypted FUSE vault notes/ host pin, design watchpoints jerboa-browser.md living plan / ticket log ``` --- a/packaging/README.md +++ b/packaging/README.md @@ -96,24 +96,44 @@ Total ≈ 299 MB (dominated by QtWebEngineCore + ICU). ```sh packaging/linux/build-tarball.sh # → qt-webengine/build/jerboa-browser-<ver>-linux-<arch>.tar.gz +BUNDLE_QT=1 packaging/linux/build-tarball.sh make static-qt # Docker build, copies ./jerboa-browser-static-qt-linux-amd64.tar.gz ``` -The tarball bundles the backend `libjerboa_browser.so`, the Jerboa `scheme/` -sources, and `build-binary.ss` + `Makefile`. When the Jerboa toolchain (Chez + -Jerboa stdlib) is present on the build host it also compiles and bundles the -self-contained native `bin/jerboa-browser` — a Jerboa program with the Chez -kernel, boot image, and the `(browser)` library all embedded (built by -`build-binary.ss`, **not** a C/C++ launcher); otherwise unpack and run `make -binary` to produce it. Either way it relies on a **system Qt 6 WebEngine** -install (documented prerequisite) rather than vendoring Qt. - -`make static-qt` wraps that Linux package path in Docker, using the -`jerboa21/jerboa` image plus Ubuntu Qt WebEngine packages, and has been verified -on `linux.cons.io` (`linux/amd64`). - -For a fully self-contained Linux artifact, run the bundle through -[`linuxdeployqt`](https://github.com/probonopd/linuxdeployqt) to produce an -AppImage — the analogue of `macdeployqt`. It has the same WebEngine helper -caveat (`QtWebEngineProcess` must find the bundled Qt libs); linuxdeployqt -handles it via `RUNPATH`/`AppRun`. Wiring that into CI is future work. +The tarball bundles the backend `libjerboa_browser.so`, the browser-owned FUSE +vault helper `libjerboa_fuse_mount.so`, the direct-USB YubiKey backend +`libyubikey_native.so`, the Jerboa `scheme/` sources, `vendor/jerboa-fuse/`, +`vendor/jerboa-yubikey/`, the support scripts, and `build-binary.ss` + +`Makefile`. +When the Jerboa toolchain (Chez + Jerboa stdlib) is present on the build host it +also compiles and bundles the self-contained native `bin/jerboa-browser` — a +Jerboa program with the Chez kernel, boot image, and the `(browser)` library all +embedded (built by `make binary`/`jerbuild`, **not** a C/C++ launcher); +otherwise unpack and run `make binary` to produce it. + +With `BUNDLE_QT=1`, the package also stages Qt libraries, platform plugins, +`QtWebEngineProcess`, WebEngine `.pak` resources, ICU data, locales, and the +shared-library dependency closure under the package root. The launcher +`bin/jerboa-browser` sets `LD_LIBRARY_PATH`, `QT_PLUGIN_PATH`, +`QML2_IMPORT_PATH`, `QT_TRANSLATIONS_PATH`, `QTWEBENGINEPROCESS_PATH`, +`QTWEBENGINE_RESOURCES_PATH`, `QTWEBENGINE_LOCALES_PATH`, and +`JERBOA_FUSE_MOUNT_LIB`, then execs `bin/jerboa-browser.bin`. `qt.conf` files +next to the native binary and `QtWebEngineProcess` point Qt at the package root. +The Chromium sandbox remains enabled. + +Browser profile persistence is opt-in with `JERBOA_BROWSER_VAULT=1`. The +browser mounts its own encrypted vault and points Qt cache/storage paths inside +that mount; do not reuse a `jsh` `,vault mount` path for the browser. The +default unlock method is YubiKey PIV through `vendor/jerboa-yubikey/lib`: the +browser prompts for the PIV PIN before the Qt profile opens and uses the card +operation as the vault passphrase. Linux packages bundle `libyubikey_native.so` +for direct USB CCID access, with PC/SC as the library fallback when available. +For first use with a blank PIV slot 9D, launch once with +`JERBOA_BROWSER_VAULT_YUBIKEY_PROVISION=1`. `JERBOA_BROWSER_VAULT_PASSPHRASE_FILE` +or `JERBOA_BROWSER_VAULT_PASSPHRASE` overrides it for recovery/testing, and +`JERBOA_BROWSER_VAULT_UNLOCK=build-secret` selects the development-only +build-local secret fallback. + +`make static-qt` always uses `BUNDLE_QT=1` inside Docker, with the +`jerboa21/jerboa` image plus Ubuntu Qt WebEngine packages, and validates the +tarball in a clean Ubuntu stage with no Qt package installed. --- a/packaging/linux/build-tarball.sh +++ b/packaging/linux/build-tarball.sh @@ -2,15 +2,13 @@ # # build-tarball.sh — package the Jerboa Browser for Linux. # -# Builds the Qt WebEngine FFI backend (libjerboa_browser.so) via CMake, then — -# when the Jerboa toolchain (Chez + Jerboa stdlib) is available — compiles the -# self-contained native `jerboa-browser` binary with build-binary.ss. The -# tarball always ships the sources + build files so the binary can be built -# later with `make binary`. It relies on a SYSTEM Qt 6 WebEngine install rather -# than vendoring Qt; for a fully self-contained artifact run the staged tree -# through linuxdeployqt to get an AppImage (see packaging/README.md). +# Builds the Qt WebEngine FFI backend (libjerboa_browser.so) via CMake, then - +# when the Jerboa toolchain (Chez + Jerboa stdlib) is available - compiles the +# self-contained native `jerboa-browser` binary. With BUNDLE_QT=1, it also +# stages the Qt WebEngine runtime: Qt libraries, plugins, QtWebEngineProcess, +# Chromium resource files, and the shared-library dependency closure. # -# Env overrides: BUILD, QT_PREFIX, VERSION, JERBOA_HOME, SCHEME. +# Env overrides: BUILD, QT_PREFIX, VERSION, JERBOA_HOME, SCHEME, BUNDLE_QT. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -21,6 +19,216 @@ STAGE="$BUILD/jerboa-browser-$VERSION-linux-$ARCH" TARBALL="$STAGE.tar.gz" JERBOA_HOME="${JERBOA_HOME:-$HOME/mine/jerboa}" SCHEME="${SCHEME:-$JERBOA_HOME/.chez/bin/scheme}" +BUNDLE_QT="${BUNDLE_QT:-0}" + +copy_tree_if_exists() { + local src="$1" dst="$2" + if [ -d "$src" ]; then + mkdir -p "$dst" + cp -aL "$src"/. "$dst"/ + fi +} + +copy_named_library() { + local name="$1" path="" + path="$(ldconfig -p 2>/dev/null | awk -v n="$name" '$1 == n { print $NF; exit }' || true)" + if [ -z "$path" ]; then + for d in /lib /usr/lib /lib/* /usr/lib/*; do + if [ -f "$d/$name" ]; then + path="$d/$name" + break + fi + done + fi + if [ -n "$path" ] && [ -f "$path" ]; then + cp -L "$path" "$STAGE/lib/" + fi +} + +should_bundle_dep() { + local dep="$1" base + base="$(basename "$dep")" + case "$base" in + ld-linux*.so*|ld-musl-*.so*|linux-vdso.so.*) + return 1 + ;; + libc.so.*|libdl.so.*|libm.so.*|libpthread.so.*|librt.so.*|libresolv.so.*|libnsl.so.*|libutil.so.*|libanl.so.*) + return 1 + ;; + esac + case "$dep" in + "$STAGE"/*) + return 1 + ;; + esac + [ -f "$dep" ] +} + +copy_ldd_deps_once() { + local changed=1 elf line dep dest + for elf in "$@"; do + [ -f "$elf" ] || continue + while IFS= read -r line; do + dep= + case "$line" in + *"=>"*) + dep="${line#*=> }" + dep="${dep%% (*}" + ;; + /*) + dep="${line%% (*}" + ;; + esac + [ -n "${dep:-}" ] || continue + should_bundle_dep "$dep" || continue + dest="$STAGE/lib/$(basename "$dep")" + if [ ! -e "$dest" ]; then + cp -L "$dep" "$dest" + changed=0 + fi + done < <(ldd "$elf" 2>/dev/null || true) + done + return "$changed" +} + +copy_ldd_closure() { + while :; do + mapfile -t elf_files < <( + find "$STAGE/bin" "$STAGE/lib" "$STAGE/libexec" "$STAGE/plugins" "$STAGE/qml" \ + -type f 2>/dev/null + ) + copy_ldd_deps_once "${elf_files[@]}" || break + done +} + +patch_rpath() { + local file="$1" rpath="$2" + [ -f "$file" ] || return 0 + patchelf --set-rpath "$rpath" "$file" 2>/dev/null || true +} + +write_qt_conf() { + local path="$1" + cat > "$path" <<'EOF' +[Paths] +Prefix=.. +Libraries=lib +Plugins=plugins +Qml2Imports=qml +Translations=translations +EOF +} + +bundle_qt_runtime() { + [ "$HAVE_BIN" = 1 ] || { + echo "BUNDLE_QT=1 requires a native bin/jerboa-browser in the package" >&2 + exit 1 + } + command -v patchelf >/dev/null 2>&1 || { + echo "BUNDLE_QT=1 requires patchelf" >&2 + exit 1 + } + + echo "==> bundling Qt WebEngine runtime" + mkdir -p "$STAGE/libexec" "$STAGE/plugins" "$STAGE/qml" \ + "$STAGE/resources/qtwebengine_locales" \ + "$STAGE/resources/qtwebengine_dictionaries" + + if [ ! -x "$STAGE/bin/jerboa-browser.bin" ]; then + mv "$STAGE/bin/jerboa-browser" "$STAGE/bin/jerboa-browser.bin" + fi + cat > "$STAGE/bin/jerboa-browser" <<'EOF' +#!/bin/sh +set -eu +APPDIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +export LD_LIBRARY_PATH="$APPDIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +export QT_PLUGIN_PATH="$APPDIR/plugins" +export QML2_IMPORT_PATH="$APPDIR/qml${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" +export QT_TRANSLATIONS_PATH="$APPDIR/translations${QT_TRANSLATIONS_PATH:+:$QT_TRANSLATIONS_PATH}" +export JERBOA_BROWSER_LIB="${JERBOA_BROWSER_LIB:-$APPDIR/lib/libjerboa_browser.so}" +export QTWEBENGINEPROCESS_PATH="${QTWEBENGINEPROCESS_PATH:-$APPDIR/libexec/QtWebEngineProcess}" +export QTWEBENGINE_RESOURCES_PATH="${QTWEBENGINE_RESOURCES_PATH:-$APPDIR/resources}" +export QTWEBENGINE_LOCALES_PATH="${QTWEBENGINE_LOCALES_PATH:-$APPDIR/resources/qtwebengine_locales}" +export QTWEBENGINE_DICTIONARIES_PATH="${QTWEBENGINE_DICTIONARIES_PATH:-$APPDIR/resources/qtwebengine_dictionaries}" +export JERBOA_FUSE_MOUNT_LIB="${JERBOA_FUSE_MOUNT_LIB:-$APPDIR/lib/libjerboa_fuse_mount.so}" +export JERBOA_BROWSER_EXE="${JERBOA_BROWSER_EXE:-$APPDIR/bin/jerboa-browser.bin}" +exec "$APPDIR/bin/jerboa-browser.bin" "$@" +EOF + chmod +x "$STAGE/bin/jerboa-browser" "$STAGE/bin/jerboa-browser.bin" + write_qt_conf "$STAGE/bin/qt.conf" + write_qt_conf "$STAGE/libexec/qt.conf" + + for d in /usr/lib/*/qt6/plugins /usr/lib/qt6/plugins; do + copy_tree_if_exists "$d" "$STAGE/plugins" + done + for d in /usr/lib/*/qt6/qml /usr/lib/qt6/qml; do + copy_tree_if_exists "$d" "$STAGE/qml" + done + for d in /usr/share/qt6/translations /usr/lib/*/qt6/translations /usr/lib/qt6/translations; do + copy_tree_if_exists "$d" "$STAGE/translations" + done + + local process_path + process_path="$(find /usr -type f -name QtWebEngineProcess 2>/dev/null | head -n 1 || true)" + [ -n "$process_path" ] || { + echo "QtWebEngineProcess not found; install the Qt WebEngine runtime package" >&2 + exit 1 + } + install -m 0755 "$process_path" "$STAGE/libexec/QtWebEngineProcess" + + while IFS= read -r f; do + case "$f" in + */qtwebengine_locales/*) + cp -L "$f" "$STAGE/resources/qtwebengine_locales/" + ;; + */qtwebengine_dictionaries/*) + cp -L "$f" "$STAGE/resources/qtwebengine_dictionaries/" + ;; + *) + cp -L "$f" "$STAGE/resources/" + ;; + esac + done < <( + find /usr -type f \( \ + -name 'qtwebengine_resources*.pak' -o \ + -name 'qtwebengine_devtools_resources.pak' -o \ + -name 'icudtl.dat' -o \ + -path '*/qtwebengine_locales/*.pak' -o \ + -path '*/qtwebengine_dictionaries/*' \ + \) 2>/dev/null + ) + + copy_named_library libcrypto.so.3 + copy_ldd_closure + + patch_rpath "$STAGE/bin/jerboa-browser.bin" '$ORIGIN/../lib' + patch_rpath "$STAGE/lib/libjerboa_browser.so" '$ORIGIN' + patch_rpath "$STAGE/libexec/QtWebEngineProcess" '$ORIGIN/../lib' + for f in "$STAGE"/lib/*.so*; do + patch_rpath "$f" '$ORIGIN' + done + + [ -f "$STAGE/libexec/QtWebEngineProcess" ] || { + echo "missing bundled QtWebEngineProcess" >&2 + exit 1 + } + [ -f "$STAGE/resources/qtwebengine_resources.pak" ] || { + echo "missing bundled qtwebengine_resources.pak" >&2 + exit 1 + } + [ -f "$STAGE/plugins/platforms/libqxcb.so" ] || { + echo "missing bundled xcb platform plugin" >&2 + exit 1 + } + [ -f "$STAGE/plugins/platforms/libqoffscreen.so" ] || { + echo "missing bundled offscreen platform plugin" >&2 + exit 1 + } + [ -f "$STAGE/lib/libQt6WebEngineCore.so.6" ] || { + echo "missing bundled Qt WebEngineCore library" >&2 + exit 1 + } +} echo "==> configuring + building the FFI backend (.so)" cmake_args=(-S "$REPO/qt-webengine" -B "$BUILD") @@ -31,12 +239,38 @@ cmake --build "$BUILD" --target jerboa_browser LIB="$BUILD/libjerboa_browser.so" [ -f "$LIB" ] || { echo "expected $LIB — Linux build did not produce it" >&2; exit 1; } +echo "==> building the FUSE mount helper" +( cd "$REPO" && make libjerboa_fuse_mount.so ) +FUSE_SHIM="$REPO/libjerboa_fuse_mount.so" +[ -f "$FUSE_SHIM" ] || { echo "expected $FUSE_SHIM" >&2; exit 1; } + +echo "==> ensuring vendored YubiKey library" +( cd "$REPO" && make vendor-yubikey ) + +echo "==> building YubiKey native USB backend" +( cd "$REPO" && make lib/libyubikey_native.so ) +YUBIKEY_NATIVE="$REPO/lib/libyubikey_native.so" +[ -f "$YUBIKEY_NATIVE" ] || { echo "expected $YUBIKEY_NATIVE" >&2; exit 1; } + echo "==> staging tarball tree at $STAGE" rm -rf "$STAGE" -mkdir -p "$STAGE/lib" "$STAGE/bin" "$STAGE/scheme" +mkdir -p "$STAGE/lib" "$STAGE/bin" "$STAGE/scheme" "$STAGE/support" cp -f "$LIB" "$STAGE/lib/" -cp -f "$REPO"/scheme/*.ss "$STAGE/scheme/" 2>/dev/null || true -cp -f "$REPO/build-binary.ss" "$REPO/Makefile" "$STAGE/" +cp -f "$FUSE_SHIM" "$STAGE/lib/" +cp -f "$YUBIKEY_NATIVE" "$STAGE/lib/" +( cd "$REPO" && tar --exclude='*.so' --exclude='*.wpo' \ + --exclude='scheme/browser/build-vault-secret.ss' -cf - scheme ) | \ + ( cd "$STAGE" && tar -xf - ) +( cd "$REPO" && tar --exclude='*.so' --exclude='*.wpo' -cf - vendor/jerboa-fuse ) | \ + ( cd "$STAGE" && tar -xf - ) +( cd "$REPO" && tar --exclude='.git' --exclude='target' --exclude='*/target' \ + --exclude='*.so' --exclude='*.dylib' --exclude='*.a' --exclude='*.wpo' \ + -cf - vendor/jerboa-yubikey ) | \ + ( cd "$STAGE" && tar -xf - ) +cp -f "$REPO/build-binary.ss" "$REPO/Makefile" "$REPO/.jerbuild" "$STAGE/" +cp -f "$REPO/support/gen-browser-vault-secret.sh" \ + "$REPO/support/ensure-vendor.sh" \ + "$REPO/support/README.md" "$STAGE/support/" # The native binary embeds Chez + the (browser) library; build it when the # Jerboa toolchain is present (e.g. a developer host). CI images without Chez @@ -100,6 +334,42 @@ else echo " (unpack, then run 'make binary' with the Jerboa toolchain installed)" fi +if [ "$BUNDLE_QT" = 1 ]; then + bundle_qt_runtime +fi + +if [ "$BUNDLE_QT" = 1 ]; then + runtime_text="$(printf '%s\n' \ + " Qt WebEngine is bundled: Qt libraries, plugins, QtWebEngineProcess," \ + " Chromium resources, locales, and shared-library dependencies are staged" \ + " inside this tree. The Chromium sandbox is left enabled.")" + bin_desc="launcher" + real_bin_line=" bin/jerboa-browser.bin self-contained native Jerboa program" + bundle_layout_text="$(printf '%s\n' \ + " lib/ bundled shared libraries" \ + " libexec/QtWebEngineProcess Chromium helper process" \ + " plugins/ Qt platform/image/tls/etc. plugins" \ + " resources/ Qt WebEngine pak/ICU/locales/dictionaries")" + bin_state_line="" +else + runtime_text="$(printf '%s\n' \ + " Prerequisite: Qt 6 WebEngine runtime installed system-wide" \ + " (e.g. apt install qt6-webengine-dev libqt6webenginewidgets6, or distro" \ + " equivalent). The Chromium sandbox is left enabled.")" + bin_desc="native binary, when present" + real_bin_line="" + bundle_layout_text="" + if [ "$HAVE_BIN" = 1 ]; then + bin_state_line=" bin/jerboa-browser self-contained native binary (<url> | browse | repl | test | run <file>)" + else + bin_state_line=" bin/ (empty - build with 'make binary'; needs Chez + Jerboa)" + fi +fi +native_layout_line="" +if [ "$HAVE_NATIVE" = 1 ]; then + native_layout_line=" lib/libjerboa_native.so Rust crypto for the secure store" +fi + cat > "$STAGE/README.txt" <<EOF Jerboa Browser $VERSION (linux-$ARCH) @@ -124,22 +394,36 @@ Keys (Emacs-style: C-=Ctrl, M-=Alt; a space is a chord, e.g. C-x b is C-x C-c quit C-h b show ALL key bindings M-x run any command by name -Prerequisite: Qt 6 WebEngine runtime installed system-wide - (e.g. apt install qt6-webengine-dev libqt6webenginewidgets6, or distro - equivalent). The Chromium sandbox is left enabled. +Runtime: +$runtime_text Layout: -$([ "$HAVE_BIN" = 1 ] && echo " bin/jerboa-browser launcher: points the loader at lib/, runs the binary" || echo " bin/ (empty — build with 'make binary'; needs Chez + Jerboa)") -$([ "$HAVE_BIN" = 1 ] && echo " bin/jerboa-browser.bin self-contained native binary (<url> | browse | repl | test | run <file>)") - lib/libjerboa_browser.so Qt WebEngine backend (the native binary dlopens this) -$([ "$HAVE_NATIVE" = 1 ] && echo " lib/libjerboa_native.so Rust crypto for the secure store (dlopened at runtime)") + bin/jerboa-browser $bin_desc +$real_bin_line + lib/libjerboa_browser.so browser backend +$native_layout_line + lib/libjerboa_fuse_mount.so browser-owned encrypted vault helper + lib/libyubikey_native.so direct USB YubiKey backend +$bundle_layout_text scheme/ the (browser) Jerboa module + entry points - build-binary.ss, Makefile rebuild the native binary: \`make binary\` + support/ build-local browser vault secret generator + vendor/jerboa-fuse/ encrypted FUSE vault modules + vendor/jerboa-yubikey/ YubiKey Scheme + native backend source + build-binary.ss, Makefile build the native binary: \`make binary\` +$bin_state_line The native jerboa-browser binary embeds Chez Scheme, the boot image, and the -(browser) library; only libjerboa_browser.so + system Qt 6 are external. +(browser) library. -For a no-prerequisite AppImage, run this tree through linuxdeployqt. +Encrypted browser vault: + Set JERBOA_BROWSER_VAULT=1 to store Qt cache, cookies, local storage, and + downloads inside the browser-owned encrypted FUSE vault. The default unlock + method is YubiKey PIV: the browser prompts for the PIV PIN before opening the + Qt profile and uses the card operation as the vault passphrase. Explicit + JERBOA_BROWSER_VAULT_PASSPHRASE_FILE or JERBOA_BROWSER_VAULT_PASSPHRASE + overrides it; JERBOA_BROWSER_VAULT_UNLOCK=build-secret uses the development + build-local secret instead. For first use with a blank PIV slot 9D, launch + once with JERBOA_BROWSER_VAULT_YUBIKEY_PROVISION=1. EOF echo "==> creating $TARBALL" --- a/qt-webengine/src/browser_view.cpp +++ b/qt-webengine/src/browser_view.cpp @@ -6,8 +6,34 @@ // single-threaded, so creation and destruction happen inline on that thread. #include "browser_objects.h" +#include <QDir> #include <QUrl> +namespace { + +QString env_path(const char *name) { + const QByteArray value = qgetenv(name); + if (value.isEmpty()) return QString(); + return QString::fromLocal8Bit(value); +} + +bool ensure_dir(const QString &path, const char *label, JwbStatus *err) { + if (path.isEmpty()) { + jwb::set_last_error(std::string(label) + " is empty"); + *err = JWB_ERR_INVALID_ARGUMENT; + return false; + } + if (!QDir().mkpath(path)) { + jwb::set_last_error(std::string("could not create ") + label + ": " + + path.toStdString()); + *err = JWB_ERR_QT; + return false; + } + return true; +} + +} // namespace + namespace jwb { void *backend_context_create(uint32_t capability_flags, JwbStatus *err) { @@ -16,9 +42,40 @@ void *backend_context_create(uint32_t capability_flags, JwbStatus *err) { *err = JWB_ERR_QT; return nullptr; } - // Default ctor = off-the-record profile (no on-disk storage). install_policy - // applies capability_flags via a request interceptor and profile settings. - auto *profile = new QWebEngineProfile(); + QWebEngineProfile *profile = nullptr; + if (capability_flags & JWB_CAP_PERSISTENT_STORAGE) { + const QString storage_path = env_path("JERBOA_BROWSER_STORAGE_PATH"); + const QString cache_path = env_path("JERBOA_BROWSER_CACHE_PATH"); + const QString download_path = env_path("JERBOA_BROWSER_DOWNLOAD_PATH"); + if (storage_path.isEmpty() || cache_path.isEmpty()) { + set_last_error("persistent storage requested without browser vault paths"); + *err = JWB_ERR_INVALID_ARGUMENT; + return nullptr; + } + if (!ensure_dir(storage_path, "persistent storage path", err) || + !ensure_dir(cache_path, "cache path", err) || + (!download_path.isEmpty() && + !ensure_dir(download_path, "download path", err))) { + return nullptr; + } + QString profile_name = env_path("JERBOA_BROWSER_PROFILE_NAME"); + if (profile_name.isEmpty()) profile_name = QStringLiteral("browser-vault"); + profile = new QWebEngineProfile(profile_name); + profile->setPersistentStoragePath(storage_path); + profile->setCachePath(cache_path); + profile->setHttpCacheType(QWebEngineProfile::DiskHttpCache); + profile->setPersistentCookiesPolicy(QWebEngineProfile::AllowPersistentCookies); + if (!download_path.isEmpty()) profile->setDownloadPath(download_path); + bool ok = false; + const int cache_max = + qgetenv("JERBOA_BROWSER_HTTP_CACHE_MAX_BYTES").toInt(&ok); + if (ok && cache_max > 0) profile->setHttpCacheMaximumSize(cache_max); + } else { + // Default ctor = off-the-record profile (no on-disk storage). + profile = new QWebEngineProfile(); + } + // install_policy applies capability_flags via a request interceptor and + // profile settings. auto *c = new Context{capability_flags, profile, nullptr}; install_policy(c); return c; --- a/scheme/browser-main.ss +++ b/scheme/browser-main.ss @@ -14,7 +14,12 @@ ;;; jerboa-browser test run the (browser) test suite ;;; jerboa-browser run FILE run a Jerboa script with (browser) available -(import (chezscheme) (browser) (browser buffers) (browser commands) (std repl)) +(import (chezscheme) + (browser) + (browser buffers) + (browser commands) + (browser vault) + (std repl)) ;; Directory of argv[0], or "." when it has no slash / is unavailable. (define (exe-dir) @@ -45,8 +50,15 @@ ;; jerboa-repl evaluates in (interaction-environment); import (browser) into it ;; so browser-* is callable at the prompt with no further imports. (eval '(import (browser)) (interaction-environment)) + (eval '(import (browser vault)) (interaction-environment)) (display ";; (browser) loaded — call (browser-init) then (browser-open-context ...)\n") - (jerboa-repl)) + (let ((vault #f)) + (guard (e (#t + (when vault (browser-vault-unmount! vault)) + (raise e))) + (set! vault (browser-vault-mount-if-enabled!)) + (jerboa-repl) + (when vault (browser-vault-unmount! vault))))) (define default-url "https://example.com") @@ -87,29 +99,41 @@ ;; Set JWB_BROWSE_MS=<ms> for a headless/no-block smoke that pumps for ms, prints ;; the page title, then exits — used by tests/CI. (define (run-browse url) - (guard (e (#t (browse-fail "could not launch the browser" e))) - (let* ((s (open-browser-session)) ; init + network context + window - (win (session-window s))) - (browser-window-set-title win "Jerboa Browser") - (let ((buf (session-open-buffer s url))) ; open + focus + load the buffer - (session-update-status! s) - (browser-window-show win) - (let ((ms (and (getenv "JWB_BROWSE_MS") - (string->number (getenv "JWB_BROWSE_MS"))))) - (if ms - (begin - (browser-pump ms) - (let ((tr (browser-eval (buffer-view buf) "document.title"))) - (when (browser-ok? tr) (display (browser-value tr)) (newline))) - (close-browser-session! s) (exit 0)) - (begin - ;; Keyboard-driven: build the controller and run the keymap loop - ;; (installs the app-wide key hook, pumps Qt events, drains the - ;; key queue → emacs keymap/command dispatch) until the window is - ;; closed or C-x C-c quits. - (let ((app (make-browser-app s))) - (app-run-loop! app)) - (close-browser-session! s) (exit 0)))))))) + (let ((vault #f) (s #f)) + (define (cleanup!) + (when s + (guard (e (#t #f)) (close-browser-session! s)) + (set! s #f)) + (when vault + (browser-vault-unmount! vault) + (set! vault #f))) + (guard (e (#t (cleanup!) (browse-fail "could not launch the browser" e))) + (set! vault (browser-vault-mount-if-enabled!)) + (set! s (open-browser-session + (if vault + (browser-capabilities 'network 'persistent-storage) + (browser-capabilities 'network)))) + (let ((win (session-window s))) + (browser-window-set-title win "Jerboa Browser") + (let ((buf (session-open-buffer s url))) ; open + focus + load the buffer + (session-update-status! s) + (browser-window-show win) + (let ((ms (and (getenv "JWB_BROWSE_MS") + (string->number (getenv "JWB_BROWSE_MS"))))) + (if ms + (begin + (browser-pump ms) + (let ((tr (browser-eval (buffer-view buf) "document.title"))) + (when (browser-ok? tr) (display (browser-value tr)) (newline))) + (cleanup!) (exit 0)) + (begin + ;; Keyboard-driven: build the controller and run the keymap loop + ;; (installs the app-wide key hook, pumps Qt events, drains the + ;; key queue → emacs keymap/command dispatch) until the window is + ;; closed or C-x C-c quits. + (let ((app (make-browser-app s))) + (app-run-loop! app)) + (cleanup!) (exit 0))))))))) (define (usage port code) (display "usage: jerboa-browser [<url> | browse [<url>] | repl | test | run <file>]\n" port) new file mode 100644 --- /dev/null +++ b/scheme/browser/vault.ss @@ -0,0 +1,255 @@ +#!chezscheme +;;; (browser vault) — browser-owned encrypted storage for Qt WebEngine. +;;; +;;; The mount is created by the browser process itself, so jerboa-fuse's access +;;; controller trusts only the browser PID and descendants such as +;;; QtWebEngineProcess. A jsh `,vault mount` can still exist at a different +;;; mountpoint; it is intentionally not reused here. + +(library (browser vault) + (export + browser-vault-enabled? + browser-vault-mount-if-enabled! + browser-vault-unmount! + browser-vault-active? + browser-vault-mountpoint) + + (import (except (chezscheme) + make-hash-table hash-table? + sort sort! + printf fprintf + path-extension path-absolute? + with-input-from-string with-output-to-string + iota 1+ 1- + partition + make-date make-time) + (except (jerboa prelude) meta atom?) + (browser build-vault-secret)