"""
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.
"""
import json
import re
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 / "scripts" / "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 _granted_permissions() -> set[str]:
"""The allowlist, read out of the source rather than described here."""
match = re.search(r"GRANTED_PERMISSIONS = new Set\(\[([^\]]*)\]\)", _main())
assert match, "the permission allowlist is gone or was renamed"
return set(re.findall(r"'([^']+)'", match.group(1)))
def test_the_page_gets_no_camera_microphone_or_location():
denied = {"media", "geolocation", "midi", "midiSysex", "notifications",
"pointerLock", "openExternal", "clipboard-read", "hid", "serial",
"usb", "idle-detection", "window-management"}
assert not (_granted_permissions() & denied)
def test_video_may_go_fullscreen():
"""
The regression this replaced a blanket denial to fix, and the reason it is
worth a test: **a denied `fullscreen` does not reject.** Chromium's own
video controls ask for it, `requestFullscreen()` returns a promise that
never settles, and the button does nothing with no error raised anywhere.
Nothing observable says "permission" — so nothing would have led back here.
"""
assert "fullscreen" in _granted_permissions()
def test_both_permission_handlers_answer_from_the_same_list():
"""`Permissions.query` takes the check handler and a request takes the
other; two lists would eventually disagree about what the page may do."""
source = _main()
for handler in ("setPermissionRequestHandler", "setPermissionCheckHandler"):
assert handler in source
after = source.split(handler, 1)[1][:400]
assert "GRANTED_PERMISSIONS" in after, (
f"{handler} decides on its own rather than from the allowlist")
# ── 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
# Comments stripped, all of them, rather than skipping past the first
# `-->`. The page's only mention of a policy is the comment explaining why
# it is not here, so the check has to see the markup with every comment
# gone — the earlier version took `split("-->")[1]`, which meant adding a
# second comment anywhere above made it read that comment's own text and
# fail on correct markup. A guard that depends on how many comments precede
# it is not guarding the thing it names.
markup = re.sub(r"", "", INDEX.read_text(encoding="utf-8"),
flags=re.S)
assert "Content-Security-Policy" not in markup, \
"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()
# The array mixes plain strings and one `${RECAPTCHA_SRC}` template literal;
# resolve the constant so every directive reads as plain text.
rec = re.search(r"const RECAPTCHA_SRC = '([^']*)'", source)
match = re.search(r"const CSP = \[(.*?)\]\.join", source, re.S)
assert match, "no CSP constant in the main process"
body = match.group(1)
if rec:
body = body.replace("${RECAPTCHA_SRC}", rec.group(1))
# Skip the `//` comments inside the array. Reading one as a directive is
# the mistake this file already records against the packaged unit test,
# which matched the comment explaining why `User=` was absent: parse
# directives, not text. A comment line here yielded a phantom `//`
# directive the moment one was written.
lines = [line.strip() for line in body.splitlines()]
return "; ".join(
line.strip('`",').strip('`"')
for line in lines if line and not line.startswith("//"))
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: nothing the hub returns is ever executed. The only script sources are
'self', the wasm eval token, and the two reCAPTCHA hosts (see the next
test) — never a bare `https:` scheme, which would let the hub's own origin
serve script.
"""
connect = _directive("connect-src")
assert "https:" in connect and "wss:" in connect
script = _directive("script-src")
assert script, "no script-src directive"
sources = script.split()[1:] # drop the "script-src" keyword itself
allowed = {
"'self'", "'wasm-unsafe-eval'",
"https://www.google.com", "https://www.gstatic.com",
}
assert set(sources) <= allowed, \
f"unexpected script-src source: {set(sources) - allowed}"
assert "https:" not in sources, "a bare https: scheme lets the hub serve script"
assert "'unsafe-eval'" not in script.replace("'wasm-unsafe-eval'", "")
assert "default-src 'none'" in _policy()
def test_recaptcha_is_the_only_third_party_and_stays_scoped_to_it():
"""
reCAPTCHA gates sign-up in the app the same way it does in the browser.
www.google.com and www.gstatic.com are allowed under script-src, frame-src
and img-src for that — and no other external origin appears anywhere in the
policy. Remove this expectation only alongside the reCAPTCHA widget.
"""
hosts = {"https://www.google.com", "https://www.gstatic.com"}
for directive in ("script-src", "frame-src", "img-src"):
srcs = set(_directive(directive).split()[1:])
assert hosts <= srcs, f"{directive} is missing a reCAPTCHA host"
for part in _policy().split(";"):
for tok in part.strip().split()[1:]:
if tok.startswith(("http://", "https://")):
assert tok in hosts, f"unexpected external origin in CSP: {tok}"
def test_the_pdf_preview_has_both_permissions_it_needs():
"""
The application shows a PDF the same way the browser does, and needs the
same two permissions to do it — this is where it was missing them.
`files-app.js` decrypts the file in the renderer and hands it to
`