Sparsh

Security

What is encrypted by what, what a paired peer can do to you, and what is still open.

The design goal: the only thing leaving your device unencrypted is what your operating system already sees.

Your files meet that goal. They ride a DTLS handshake the two devices perform directly, and no relay ever holds a key for it. Your signalling meets it between devices that have paired. It does not meet it when somebody types a six-digit code, and that is said plainly below rather than buried.

What is encrypted, by what

LayerProtectsBy
HTTPSthe page itselfTLS 1.3
Signallingthe SDP offer, answer and ICE candidatesAES-256-GCM over the relay's own TLS, and between paired devices the SDP is signed
Data channelfiles, messages, MCPDTLS, fingerprint pinned in the SDP
Mediaaudio, video, screenSRTP from the DTLS handshake

DTLS keys are never sent anywhere. The peers exchange certificate fingerprints inside the SDP and then perform a fresh handshake directly between themselves. A relay that drops or reorders events can break a connection. It cannot read one.

Between paired devices, the SDP is signed

Every device holds a secp256k1 keypair and its public key is its identity. Once two devices have paired, each knows the other's key, so the SDP carrying the DTLS fingerprint is signed by the sender and verified by the receiver before WebRTC is ever given it.

That matters most on the signalling server path, where the SDP is not encrypted end to end. The server can still read an offer, which it always could. It can no longer substitute one: swapping the fingerprint to sit invisibly in the middle now fails verification and the connection is refused.

Proven rather than asserted. The end-to-end test was run with the check removed, and the tampered SDP was accepted and the file sent. With the check in place the same run is refused.

First contact without a code

sparsh invite prints a link carrying 128 bits of secret and this device's public key, both in the URL fragment, which RFC 3986 keeps off the request line. Neither reaches a server or an access log.

That closes first contact in both directions. The joiner verifies the host's offer against the key it was handed, and the host verifies the joiner through a proof of the secret bound to the joiner's own key, so a proof captured in flight cannot be replayed by a different device. The invitation is single use.

An invitation offered over a transport that cannot check it is refused, not quietly downgraded. A link that looked strong while verifying nothing would be worse than the code it replaces, because it would not admit what it is.

The typed code is still weak

A six-digit code is 900,000 values, and the relay topic is a plain hash of it. An attacker computes all 900,000 hashes once, seconds of CPU, subscribes to every one on the public relays, and waits. Any session that appears is matched straight back to its code, and the code is the encryption key.

The attack is passive, precomputed and reusable. It needs no guessing and no race.

What it yields passively: the whole SDP, both peers' addresses and the DTLS fingerprints. The file bytes stay unreadable, because DTLS performs its own key exchange. What it yields actively: the file bytes, because whoever holds the code can publish a forged answer and become the far end.

Use the link. It is why the link exists.

What exposing an MCP server means

sparsh expose <device> -- <command> runs that command as you, with your file permissions, whenever that one paired device opens a tools channel. The device is authenticated, so the only question is whether you trust the agent on it with that server. Choose what you expose the way you choose what you sudo: a filesystem server rooted at one directory, not a shell.

What bounds it: no pairing has the grant by default; the grant names one server; the server starts with a minimal environment rather than yours, so an API key in your shell does not travel; one channel per session; a 10 MiB cap per message; one audit line per call with the peer, the tool and the size, never the arguments; and unexpose closes a live channel immediately.

Toward the device consuming tools, two requests never cross: sampling/createMessage, so a remote server cannot spend your model, and roots/list, so it cannot read your project layout.

What is not solved

Said plainly, because a list of mitigations with the gaps left out is worse than no list.

  • There is no accept prompt for an incoming file. A paired device can send and it starts arriving. The consent was the pairing.
  • The peer chooses the filename, and it reaches your downloads. The name is checked for path separators, not for honesty.
  • A remote server's replies are text in your agent's context. The transport authenticates the device, not the intentions of the software on it.
  • None of this decides who you pair with. It bounds the damage.

On this page