""" The desktop shell's security contract, pinned by reading its source. There is no npm on the development machine, so the Electron application cannot be installed or launched here. That is stated plainly rather than worked around: **nothing below proves the app runs.** What it does prove is that the properties the design depends on are present in the source, and it fails if one is removed — which is the same treatment `test_downloads.py` gives the three browser-specific save paths, for the same reason. Every assertion here corresponds to a sentence in `docs/desktop-client-v1.md` §3. Weak evidence, and the only evidence available without a packaged build; a person with an installed client is what confirms the rest. """ from pathlib import Path import pytest CLIENT = Path(__file__).resolve().parents[2] / "meshbay-client" MAIN = CLIENT / "src" / "main.js" PRELOAD = CLIENT / "src" / "preload.js" INDEX = CLIENT / "build" / "index.html" pytestmark = pytest.mark.skipif( not MAIN.exists(), reason="desktop client sources not present") def _main() -> str: return MAIN.read_text(encoding="utf-8") def _preload() -> str: return PRELOAD.read_text(encoding="utf-8") # ── The renderer is confined ──────────────────────────────────────────────── @pytest.mark.parametrize("setting", [ "sandbox: true", "contextIsolation: true", "nodeIntegration: false", ]) def test_the_renderer_keeps_its_sandbox(setting): """ Electron with these keeps the Chromium renderer sandbox — the strongest available, and the reason "native costs the browser sandbox" is false for this shell. Without contextIsolation the preload's objects are reachable and mutable from page script, which would make the bridge decorative. """ assert setting in _main(), f"{setting} is missing from the window" def test_the_interface_is_never_loaded_from_the_hub(): """ The whole reason this application exists. A shell pointing a WebView at the hub's /app/ is a browser with a different icon and fixes nothing (T3). """ source = _main() assert "loadURL(`${SCHEME}://" in source or "loadURL('app://" in source assert "loadURL('http" not in source and 'loadURL("http' not in source assert "loadURL(`http" not in source def test_navigation_away_from_the_package_is_refused(): source = _main() assert "will-navigate" in source assert "setWindowOpenHandler" in source assert "event.preventDefault()" in source def test_no_permission_is_granted_to_the_page(): """Nothing here needs a camera, a microphone or a location.""" assert "setPermissionRequestHandler" in _main() assert "callback(false)" in _main() # ── The custom scheme ─────────────────────────────────────────────────────── @pytest.mark.parametrize("privilege", [ "standard: true", "secure: true", "supportFetchAPI: true", "stream: true", ]) def test_the_scheme_is_privileged(privilege): """ `secure` is what makes it a secure context, and without it **the whole of `crypto.subtle` is undefined** — measured, not assumed: the first probe loaded a `data:` URL and every algorithm failed with TypeError, AES-GCM included. `standard` gives a real origin, so IndexedDB survives an update instead of being keyed to something that moves. An earlier version of this docstring said `secure` was what let the service worker register. That is wrong: Chromium refuses to register a worker on a custom scheme whatever its privileges — "The URL protocol of the current origin ('app://meshbay') is not supported". The application therefore has no service worker and does not need one; it saves files through a native dialog, which is better than the path the worker exists to provide. """ assert privilege in _main(), f"{privilege} missing from the scheme privileges" def test_the_protocol_handler_cannot_be_walked_out_of(): """ The renderer parses decrypted content from nodes, which is attacker-controlled input. A traversal here would hand it the filesystem. """ source = _main() assert "path.resolve(UI_DIR" in source assert "startsWith(root + path.sep)" in source assert "status: 404" in source # ── Content Security Policy ───────────────────────────────────────────────── def test_the_policy_is_sent_as_a_header(): """ A policy cannot carry `frame-ancestors`, and having it there means one directive of the policy is decoration. The handler is also the only thing that serves the interface, so this is one source rather than two. """ source = _main() assert "'Content-Security-Policy': CSP" in source assert "Content-Security-Policy" not in INDEX.read_text(encoding="utf-8") \ .split("-->")[1], "the packaged page still carries a policy of its own" def test_the_policy_keeps_wasm_unsafe_eval(): """ The bundle key is Argon2id in WebAssembly. A policy that forbids it does not degrade anything — it locks every user out of their keys. """ assert "'wasm-unsafe-eval'" in _directive("script-src") def _policy() -> str: """ The policy the protocol handler sends, read out of the CSP constant. Not a tag: `frame-ancestors` is ignored there, and a directive that silently does nothing is worse than one that is absent. Chromium said so in the console the first time the application was launched. """ import re source = _main() match = re.search(r"const CSP = \[(.*?)\]\.join", source, re.S) assert match, "no CSP constant in the main process" return "; ".join( line.strip().strip('",').strip('"') for line in match.group(1).splitlines() if line.strip()) def _directive(name: str) -> str: for part in _policy().split(";"): part = part.strip() if part.startswith(name + " "): return part return "" def test_the_hub_is_reachable_but_never_executable(): """ connect-src allows the hub's API and its signaling socket. script-src does not include it: nothing the hub returns is ever executed. """ connect = _directive("connect-src") assert "https:" in connect and "wss:" in connect script = _directive("script-src") assert script, "no script-src directive" assert "https:" not in script, "the hub can serve script under this policy" assert "'unsafe-eval'" not in script.replace("'wasm-unsafe-eval'", "") assert "default-src 'none'" in _policy() # ── The bridge ────────────────────────────────────────────────────────────── def test_the_bridge_is_the_only_way_in(): source = _preload() assert "contextBridge.exposeInMainWorld" in source # Handing the raw ipcRenderer to the page would expose every channel in the # main process, named or not. assert "exposeInMainWorld('meshbay', ipcRenderer" not in source assert "ipcRenderer)" not in source.replace("require('electron');", "") def test_the_renderer_never_names_a_path(): """ It asks for a dialog and receives an opaque id; the main process holds the handle. A channel that took a path from the renderer and wrote to it would be the whole confinement undone. """ source = _preload() assert "save:begin" in source assert "handle.id" in source assert "filePath" not in source, "the preload passes a filesystem path around" def test_the_hub_address_is_not_fetched_synchronously_over_ipc(): """ `platform.hubBase()` runs while the module graph is loading, before anything can await. Synchronous IPC would block the renderer on every call for a value that cannot change within a run. """ source = _preload() assert "--meshbay-hub=" in source assert "sendSync" not in source def test_plain_http_is_refused_except_to_loopback(): """Anywhere else it would put the session token on the wire in clear.""" source = _main() assert "must be https" in source # The guard is a regular expression, so the dot is escaped in the source. assert "127\\." in source and "localhost" in source # ── One interface, one source ─────────────────────────────────────────────── def test_the_interface_is_copied_not_forked(): """ §2.7: the hub's static directory is the single source. A silent fork is the only real way to end up maintaining the interface twice, so the copy is generated and the generated tree is not committed. """ sync = (CLIENT / "build" / "sync-ui.js").read_text(encoding="utf-8") assert "meshbay-hub" in sync and "static" in sync assert "rmSync" in sync, "a stale file could survive a rebuild" gitignore = (CLIENT.parents[1] / ".gitignore").read_text(encoding="utf-8") assert "meshbay-client/ui/" in gitignore, ( "the generated copy is committed, which is how a fork begins") def test_the_packaged_page_loads_the_shared_modules(): """The app's index.html is its own — the hub's carries a /a// prefix that would point back at the hub — but it must load the same files.""" page = INDEX.read_text(encoding="utf-8") for module in ("keyderive.js", "crypto.js", "transport.js", "app.js", "style.css", "argon2.min.js"): assert module in page, f"{module} is not loaded by the packaged page" assert "/a/" not in page, "the packaged page points at the hub's asset prefix"