MeshVault Docs
Security

The QR carries no secret.
Your approval is the key.

A pairing code on a screen is a photograph waiting to happen. So the code on screen is worth nothing on its own: it points at an invite, and the only thing that enrols a device is you comparing six digits and tapping approve on an authenticated session.

READ FROM THE SOURCE 2026-07-27

The handshake

Four moves, one of them yours.

  1. The node mints an invite. The QR encodes https://<node>/pair#<invite id> and nothing else. Minting sits behind the session gate.
  2. The phone scans, posts an enrolment request, and displays six digits.
  3. The node shows the same six digits beside Approve and Reject.
  4. You compare and tap. That tap is the authentication.

Photographing the screen therefore gains an attacker a raised request, which surfaces on your machine as one you did not start, showing a code that matches nothing in your hand. Rejecting it costs you a tap.

The two verbs live on separate paths for exactly this reason. Minting an invite requires a session. Raising a request does not, and cannot, because a phone that has never authenticated to the node is the entire point of the flow. Raising a request grants nothing.

Design choices

Four decisions worth naming.

The id rides in the fragment

A URL fragment is never sent to a server. The invite id stays out of access logs, out of Referer headers and out of every proxy in between. It is only a pointer either way.

One error message, on purpose

An expired invite and an invite that never existed produce the same rejection. Distinguishing them would turn the endpoint into an oracle for guessing ids.

The invite burns on approval

One QR, one device. A code that keeps working is a screenshot that quietly becomes a standing enrolment credential.

Short clocks, hard caps

Invites last 10 minutes, requests 3, and at most 8 requests can be pending at once. A flood of prompts on your screen is its own attack.

The six digits come from a rejection-free random draw, so every code from 000000 to 999999 is equally likely. The poll a waiting phone makes returns state, code and expiry only. Device internals never appear in that response.

Where it stops

The limits, in plain words.

A security page that only lists strengths is a marketing page. These are the edges of the model as the code stands on 2026-07-27.

Enrolment is not a connection

The mesh module owns identity and enrolment. It establishes no tunnel, assigns no addresses and routes no traffic. A device in the ledger is a record that you approved something, and that is the whole of it.

The mesh private key signs nothing yet

Creating a mesh generates an Ed25519 keypair and writes the private key to the store, where it stays and never enters an API response. Nothing in the current code signs with it. It is an identity waiting for a protocol, not a key in use.

An enrolled device holds no key of its own

The fingerprint on a device record is derived from the pairing request itself. It identifies the enrolment event and lets you tell two devices apart in a list. It is not a credential the phone holds and cannot prove anything later.

Middleware is not the boundary

The edge middleware shape-checks a cookie and nothing more. A hand-forged 43-character string of the right alphabet passes it. The real check runs inside each route handler, hashes the presented token and compares against stored hashes with a timing-safe compare. The source says this in its own header rather than leaving it to be discovered.

Same-machine processes are not defended against

The server binds loopback and the adversary the threat model names is other software running as the same user on the same box. Such a process can overwrite the credential record. Defending it needs an OS keyring and is out of scope today.

Two smaller ones, written down rather than hidden

The login lockout after ten failures is held in memory, so restarting the daemon clears it; scrypt at roughly 100ms per attempt is the real cost imposed on guessing. And the session cookie is not marked Secure, deliberately, because a Secure cookie is dropped over plain http on loopback and the gate would silently never log anyone in. It is httpOnly and same-site lax.

Reporting

Where to send a finding.

Email contact@meshvault.ai with a subject line starting with Security. Machine-readable contact lives at /.well-known/security.txt. The scope, response window and the fact that there is no paid bounty are on the security page.

Include the URL or asset, the steps to reproduce, and what an attacker could do with it. Please do not run destructive testing against production.